Skip to content

Commit 5589607

Browse files
committed
feat(task-board-demo): implement Kanban-style task board with view transitions
1 parent 01a6200 commit 5589607

15 files changed

Lines changed: 1099 additions & 1779 deletions

astro.config.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
} from "./src/markdown/satteri-hast-plugins";
66

77
import { satteri } from '@astrojs/markdown-satteri';
8-
import {
9-
viewTransitions,
10-
remarkEndOfMarkdown,
11-
} from "astro-vtbot/starlight-view-transitions";
8+
import { viewTransitions, hastMarkEndOfMarkdown } from "astro-vtbot/starlight-view-transitions";
129

1310
import vtbot from "astro-vtbot";
1411
import d2 from "astro-d2";
@@ -32,8 +29,8 @@ export default defineConfig({
3229
},
3330
},
3431
processor: satteri({
35-
mdastPlugins: [remarkEndOfMarkdown],
3632
hastPlugins: [
33+
hastMarkEndOfMarkdown("hurz"),
3734
createExternalLinksPlugin({
3835
target: "_blank",
3936
marker: "↗",
@@ -51,7 +48,7 @@ export default defineConfig({
5148
plugins: [
5249
viewTransitions({
5350
declarativeNames:
54-
":is(h2, h3):not(.no-vtbag-decl *) = vtbag-h-; :is(starlight-toc span):not(.no-vtbag-decl *) = vtbag-toc~",
51+
":is(h2, h3):not(.no-vtbag-decl *) = vtbag-h-; :is(starlight-toc span):not(.no-vtbag-decl *) = vtbag-toc~; :root:not(:has(.hero)) .site-title img, :root:has(.hero) .hero img = site-logo",
5552
})
5653
],
5754
components: {
@@ -218,7 +215,7 @@ function sidebar() {
218215
{
219216
label: "@vtbag",
220217
link: "/vtbag/",
221-
},
218+
},
222219
{
223220
label: "Basic Information",
224221
items: [

package-lock.json

Lines changed: 889 additions & 1679 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,30 @@
1414
"postinstall": "patch-package"
1515
},
1616
"devDependencies": {
17-
"@astrojs/starlight": "^0.41.3",
18-
"@cloudflare/workers-types": "^5.20260721.1",
17+
"@astrojs/starlight": "^0.41.7",
18+
"@cloudflare/workers-types": "^5.20260811.1",
1919
"@expressive-code/plugin-collapsible-sections": "^0.44.1",
2020
"@expressive-code/plugin-line-numbers": "^0.44.1",
2121
"@inox-tools/portal-gun": "^3.0.0",
22-
"@playwright/test": "^1.61.1",
22+
"@playwright/test": "^1.62.1",
2323
"@types/canvas-confetti": "^1.9.0",
2424
"@types/dompurify": "^3.0.5",
25-
"@types/node": "^26.1.1",
25+
"@types/node": "^26.2.0",
2626
"@vtbag/cam-shaft": "^1.0.6",
2727
"@vtbag/element-crossing": "^1.1.0",
2828
"@vtbag/inspection-chamber": "^1.0.24",
2929
"@vtbag/turn-signal": "^1.3.1",
3030
"@vtbag/utensil-drawer": "^1.2.17",
31-
"astro": "^7.1.3",
31+
"astro": "^7.2.0",
3232
"astro-d2": "^0.13.1",
33-
"astro-vtbot": "^3.0.0",
34-
"astro-webrings": "^0.1.4",
35-
"baseline-status": "^1.1.1",
33+
"astro-vtbot": "^3.0.1",
34+
"astro-webrings": "^0.1.5",
35+
"baseline-status": "^1.2.0",
3636
"canvas-confetti": "^1.9.4",
3737
"compromise": "^14.16.0",
38-
"dompurify": "^3.4.12",
38+
"dompurify": "^3.4.13",
3939
"html-entities": "^2.6.0",
40-
"node-html-parser": "^9.0.0",
40+
"node-html-parser": "^9.0.1",
4141
"patch-package": "^8.0.1",
4242
"postcss-active-view-transition-type": "^0.0.7",
4343
"rehype-autolink-headings": "^7.1.0",
@@ -54,12 +54,9 @@
5454
"rollup-plugin-visualizer": "^7.0.1",
5555
"sharp": "^0.35.3",
5656
"starlight-image-zoom": "^0.15.0",
57-
"tsx": "^4.23.1",
58-
"typescript": "^6",
57+
"tsx": "^4.23.12",
58+
"typescript": "^6.0.3",
5959
"unified": "^11.0.5",
60-
"wrangler": "^4.112.0"
61-
},
62-
"dependencies": {
63-
"@astrojs/check": "^0.9.9"
60+
"wrangler": "^4.120.1"
6461
}
6562
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
diff --git a/node_modules/@astrojs/starlight/components/ThemeSelect.astro b/node_modules/@astrojs/starlight/components/ThemeSelect.astro
2+
index d3e9670..ed6c93c 100644
3+
--- a/node_modules/@astrojs/starlight/components/ThemeSelect.astro
4+
+++ b/node_modules/@astrojs/starlight/components/ThemeSelect.astro
5+
@@ -47,13 +47,13 @@ import Select from './Select.astro';
6+
matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
7+
8+
/** Update select menu UI, document theme, and local storage state. */
9+
- function onThemeChange(theme: Theme): void {
10+
+ function onThemeChange(theme: Theme, source: 'initial' | 'media' | 'select'): void {
11+
StarlightThemeProvider.updatePickers(theme);
12+
13+
const resolvedTheme = theme === 'auto' ? getPreferredColorScheme() : theme;
14+
const event = new CustomEvent('starlight:theme-change', {
15+
cancelable: true,
16+
- detail: { theme, resolvedTheme },
17+
+ detail: { theme, resolvedTheme, source },
18+
});
19+
20+
const handled = !document.dispatchEvent(event);
21+
@@ -66,16 +66,16 @@ import Select from './Select.astro';
22+
23+
// React to changes in system color scheme.
24+
matchMedia(`(prefers-color-scheme: light)`).addEventListener('change', () => {
25+
- if (loadTheme() === 'auto') onThemeChange('auto');
26+
+ if (loadTheme() === 'auto') onThemeChange('auto', 'media');
27+
});
28+
29+
class StarlightThemeSelect extends HTMLElement {
30+
constructor() {
31+
super();
32+
- onThemeChange(loadTheme());
33+
+ onThemeChange(loadTheme(), "initial");
34+
this.querySelector('select')?.addEventListener('change', (e) => {
35+
if (e.currentTarget instanceof HTMLSelectElement) {
36+
- onThemeChange(parseTheme(e.currentTarget.value));
37+
+ onThemeChange(parseTheme(e.currentTarget.value), 'select');
38+
}
39+
});
40+
}

src/content/docs/basics/api.mdx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ head:
88
content: "/api-og.png"
99
---
1010

11-
The View Transition API is a rather new API. In this section you can find…
11+
The View Transition API is a rather new browser API. It gives you three cool things:
12+
- [Pseudo-elements](/basics/pseudos/) that let you simultaneously show elements from the old and from the new DOM.
13+
- Automatically generated cross-fade and morph [animations](/basics/default-animations/) that let you animate between the old and new images.
14+
- [Continuous synchronization](/basics/default-animations/#view-transition-super-power) of content and some styles from the captured elements of the new DOM to their pseudo-elements, so that dynamic content changes become visible at once, and morph animations react to updates of the new elements' position in real time.
15+
16+
There are two ways to trigger a view transition:
17+
- You can call [`startViewTransition()`](/basics/javascript/#startviewtransition) on the document or an element for global respectively scoped, same-document view transitions.
18+
- You can enable cross-document view transitions with the `@view-transition` CSS at-rule.
19+
20+
In this section you can find…
1221

1322
* …some [basic examples](/basics/examples/) of same-document and cross document view transitions.
1423
* …information about the [structure of the pseudo-elements](/basics/pseudos/) of the API.

src/content/docs/basics/default-animations.mdx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,27 @@ Additional technical details:
2525
When you combine the fade effects with non overlapping movement of the old and new images, you might want to reconsider the `plus-lighter` setting, as it can lead to unexpected visual results. In such cases, you can override the default behavior by setting `mix-blend-mode: normal` on the image pair.
2626
:::
2727

28-
* In the description of morph animations above, *position* was used for simplicity. In reality, the group animation accounts for both the element's position and any applied CSS transformations, such as those defined by `matrix3d` or specialized forms like `rotate`. When you read *position* in the following text, it always means *…and the applied transformations*. And did I mention backdropFilter, yet? So the current set is `width`, `height`, `transform` and `backdropFilter`.
28+
* In the description of morph animations above, *position* was used for simplicity. In reality, the group animation accounts for both the element's position and any applied CSS transformations, such as those defined by `matrix3d` or specialized forms like `rotate`.
29+
30+
When you read *position* in the following text, it always means *…and the applied transformations*. And did I mention backdropFilter, yet? So the current set is `width`, `height`, `transform` and `backdropFilter`.
2931

3032

3133

32-
* The `::view-transition-group` pseudo element is invisible. And so will be animations applied to it, yes? Good point. There are only two visible images. The old and the new image. Neither `::view-transition-image-pair` nor `::view-transition-group` or `::view-transition-group-children` or `::view-transition` are visible with the default styling. The effect of the group animation indirect. Moving the group also moves the images inside it. Changing the group's width also adjusts the image sizes, as the default styling keeps them the same width and scales the height to maintain the original aspect ratio.
34+
* The `::view-transition-group` pseudo element is invisible. And so will be animations applied to it, yes? Good point. There are only two visible images. The old and the new image. Neither `::view-transition-image-pair` nor `::view-transition-group` or `::view-transition-group-children` or `::view-transition` are visible with the default styling.
3335

34-
By default, both types of animations, entry/exit and group/morph, occur simultaneously, though they may not always be noticeable. For example, if elements sharing the same view transition name have identical size and position before and after the transition, the group animation will result in no visible change. Similarly, if the old and new images are identical, only the group animation will be apparent, with the exit and entry animations having no visible effect.
36+
The effect of the group animation is an indirect one. Moving the group also moves the images inside it. Changing the group's width also adjusts the image sizes, as the default styling keeps them the same width and scales the height to maintain the original aspect ratio.
37+
38+
By default, both types of animations, entry/<wbr/>exit and group/<wbr/>morph animations, occur simultaneously, though they may not always be noticeable.
39+
40+
For example, if elements sharing the same view transition name have identical size and position before and after the transition, the group animation will result in no visible change. Similarly, if the old and new images are identical, only the group animation will be apparent, with the exit and entry animations having no visible effect.
3541

3642
## Morphing Animation Details
3743

38-
The above description states that the group animation morphs from the size and position values of the old image to the values of the new image. But how does this work exactly? Morph animations are only defined for groups that have both, the old and the new image. The height, width and position of the group pseudo-element is copied over from the old element. The image pair, the only child of the group, fills the whole group. The old and the new image are inside the image pair, where the new image is rendered above the old image. If the new image is fully transparent at the beginning of the animation, the screen shows the old image in its old position.
44+
The above description states that the group animation morphs from the size and position values of the old image to the values of the new image. But how does this work exactly?
45+
46+
Morph animations are only defined for groups that have both, the old and the new image. Initially the group's animation starts with the height, width and position of underlying old element. The image pair, the only child of the group, fills the whole group. The old and the new image are inside the image pair, where the new image is rendered above the old image. As the new image is fully transparent at the beginning of the animation, the screen shows the old image in its old position.
3947

40-
During the morph animation, the group moves to the final position of the new image. Doing so, it takes the image-pair and the images within with it. At the same time, the width and height of the group element transitions towards the width and height of the new image. This change in size might force the images inside the image-pair to react with size changes as well.
48+
During the morph animation, the group moves to the current position of the new image. Doing so, it takes the image-pair and the images within with it. At the same time, the width and height of the group element transitions towards the current width and height of the new image. This change in size might force the images inside the image-pair to react with size changes as well.
4149

4250
Let's assume we start with an old image with an aspect ratio of 2:3 and morph this into a 3:2 new image.
4351

@@ -74,4 +82,30 @@ Finally the last rows shows the combined effect and also hints a bit on the fade
7482
### Corollaries
7583
The captured images for the automatically added `::view-transition-group(root)` on the `<html>` element both have the [size of the viewport](/basics/pseudos/#important-exception) and therefore are independent of the size of the page. The group animation morphs the viewport into an area of same position and size. Thus only the cross-fade animation is observable.
7684

77-
With the default styles from the user-agent stylesheet, when morphing between elements with the same width before and after the transition, the old and new images will maintain their original size during the transition, even if their heights differ significantly. You can observe this in the main content area when navigating between short and tall pages on this site.
85+
With the default styles from the user-agent stylesheet, when morphing between elements with the same width before and after the transition, the old and new images will maintain their original size during the transition, even if their heights differ significantly. You can observe this in the main content area of this site when navigating between short and tall pages.
86+
87+
### View Transition Super Power
88+
89+
There is one not so well known feature of the View Transition API that is worth mentioning. You might have heard, that the new image isn't a screenshot of its underlying DOM element but a "live" image or replaced element that follows content changes of the underlying element.
90+
91+
This is only half part of the story. The content of the `::view-transition-new()` pseudo-element is refreshed in each frame while the view transition is running. But there are also some style properties that are continuously copied from the new element to the group of its pseudo-element (if that new element exists). These are:
92+
```css
93+
width: ... /* of the new element */;
94+
height: ... /* of the new element */;
95+
transform: ... /* that maps the group to the current position of the new element */;
96+
writing-mode: ... /* of the new element */;
97+
direction: ... /* of the new element */;
98+
text-orientation: ... /* of the new element */;
99+
mix-blend-mode: ... /* of the new element */;
100+
backdrop-filter: ... /* of the new element */;
101+
color-scheme: ... /* of the new element */;
102+
```
103+
The most interesting property is `transform`. In each frame it gets assigned a value that maps the group from (0, 0) inside its parent to the _current_ visual position and transform of the new element.
104+
105+
So if you resize or rotate the element with the view transition name `x` during a view transition, the group animation for the `::view-transition-group(x)` reacts to that change in real time.
106+
107+
Part of that magic is that the keyframes for the [group animation](/basics/styling/#group--morph-animation) only specify the start of the animation:
108+
109+
The `from` keyframe reflects the position and size of the old element at the time the snapshot was taken. But the end state of the animation is unspecified and is therefore replaced with the updated values of the new pseudo-element in each animation frame. Thus, the animation will always aim to reach the current position and size of the new element at its end.
110+
111+
You can use this behaviour to create some interesting effects where you [style the captured element](/basics/styling/#styling-underlying-captured-elements) to influence the pseudo-element.

0 commit comments

Comments
 (0)