@ox-content/vite-plugin 3.0.0-alpha.16 → 3.0.0-alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/index.cjs +99 -69
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +23 -39
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +23 -39
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +38 -18
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/markdown-tables.d.cts +13 -16
  10. package/dist/markdown-tables.d.cts.map +1 -1
  11. package/dist/markdown-tables.d.mts +13 -16
  12. package/dist/markdown-tables.d.mts.map +1 -1
  13. package/dist/napi.cjs +44 -0
  14. package/dist/napi.cjs.map +1 -0
  15. package/dist/napi.mjs +34 -0
  16. package/dist/napi.mjs.map +1 -0
  17. package/dist/reader-chrome-client.cjs +103 -0
  18. package/dist/reader-chrome-client.d.cts +2 -0
  19. package/dist/reader-chrome-client.d.mts +2 -0
  20. package/dist/reader-chrome-client.mjs +102 -0
  21. package/dist/reader-chrome.cjs +109 -0
  22. package/dist/reader-chrome.cjs.map +1 -0
  23. package/dist/reader-chrome.d.cts +103 -0
  24. package/dist/reader-chrome.d.mts +103 -0
  25. package/dist/reader-chrome.mjs +98 -0
  26. package/dist/reader-chrome.mjs.map +1 -0
  27. package/dist/styles/all.css +2 -0
  28. package/dist/styles/core.css +53 -1
  29. package/dist/styles/reader-chrome.css +163 -0
  30. package/dist/styles/theme-transition.css +37 -0
  31. package/dist/styles/twitter-full.css +87 -13
  32. package/dist/theme-tokens.d.cts +18 -21
  33. package/dist/theme-tokens.d.cts.map +1 -1
  34. package/dist/theme-tokens.d.mts +18 -21
  35. package/dist/theme-tokens.d.mts.map +1 -1
  36. package/dist/theme-transition-client.cjs +161 -0
  37. package/dist/theme-transition-client.d.cts +21 -0
  38. package/dist/theme-transition-client.d.mts +21 -0
  39. package/dist/theme-transition-client.mjs +160 -0
  40. package/dist/twitter-client.cjs +84 -0
  41. package/dist/twitter-client.d.cts +11 -0
  42. package/dist/twitter-client.d.mts +11 -0
  43. package/dist/twitter-client.mjs +83 -0
  44. package/dist/vitepress.cjs +5 -43
  45. package/dist/vitepress.cjs.map +1 -1
  46. package/dist/vitepress.mjs +5 -31
  47. package/dist/vitepress.mjs.map +1 -1
  48. package/package.json +46 -4
@@ -1,4 +1,3 @@
1
- //#region src/theme-tokens.d.ts
2
1
  /**
3
2
  * Free-form `--octc-*` custom properties for themes that need more than the
4
3
  * typed `colors` / `fonts` / `layout` fields.
@@ -9,7 +8,7 @@
9
8
  * accents, and surface textures purely through tokens, while a skin package
10
9
  * lays out geometry against those same tokens without knowing any color.
11
10
  */
12
- type ThemeTokens = Record<string, string>;
11
+ export type ThemeTokens = Record<string, string>;
13
12
  /**
14
13
  * The token-bearing shape of a theme.
15
14
  *
@@ -20,25 +19,25 @@ type ThemeTokens = Record<string, string>;
20
19
  * `ThemeConfig` — including the published `@ox-content/theme-color-*` and
21
20
  * `@ox-content/theme-*` packages — satisfies it.
22
21
  */
23
- interface ThemeTokenSource {
24
- tokens?: ThemeTokens;
25
- darkTokens?: ThemeTokens;
26
- extends?: ThemeTokenSource;
22
+ export interface ThemeTokenSource {
23
+ tokens?: ThemeTokens;
24
+ darkTokens?: ThemeTokens;
25
+ extends?: ThemeTokenSource;
27
26
  }
28
27
  /**
29
28
  * Options for {@link renderThemeTokenCss}.
30
29
  */
31
- interface RenderThemeTokenCssOptions {
32
- /**
33
- * Keeps only the tokens whose name passes the predicate. Names arrive without
34
- * the `--octc-` prefix, so `(name) => name.startsWith("syntax-")` reuses a
35
- * color scheme's highlighter palette without adopting its page colors,
36
- * typography, or layout policy.
37
- *
38
- * Filtering runs per layer, before merging, so a token a later layer would
39
- * have overridden is dropped along with the override.
40
- */
41
- include?: (name: string) => boolean;
30
+ export interface RenderThemeTokenCssOptions {
31
+ /**
32
+ * Keeps only the tokens whose name passes the predicate. Names arrive without
33
+ * the `--octc-` prefix, so `(name) => name.startsWith("syntax-")` reuses a
34
+ * color scheme's highlighter palette without adopting its page colors,
35
+ * typography, or layout policy.
36
+ *
37
+ * Filtering runs per layer, before merging, so a token a later layer would
38
+ * have overridden is dropped along with the override.
39
+ */
40
+ include?: (name: string) => boolean;
42
41
  }
43
42
  /**
44
43
  * Renders a theme's `--octc-*` tokens as a standalone stylesheet.
@@ -60,7 +59,7 @@ interface RenderThemeTokenCssOptions {
60
59
  * Layers compose left to right and each layer's `extends` chain is flattened
61
60
  * base-first, matching how `resolveTheme()` stacks a skin and a color scheme.
62
61
  */
63
- declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[], options?: RenderThemeTokenCssOptions): string;
62
+ export declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[], options?: RenderThemeTokenCssOptions): string;
64
63
  /**
65
64
  * Renders light and dark token records as the three selectors the SSG runtime
66
65
  * switches between: an explicit `[data-theme="dark"]` opt-in, the OS
@@ -69,7 +68,5 @@ declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[
69
68
  * Emitted after the typed color variables and before the theme's own `css`, so
70
69
  * a token can override a typed color and raw `css` can override a token.
71
70
  */
72
- declare function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string;
73
- //#endregion
74
- export { RenderThemeTokenCssOptions, ThemeTokenSource, ThemeTokens, renderThemeTokenCss, tokensToCss };
71
+ export declare function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string;
75
72
  //# sourceMappingURL=theme-tokens.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme-tokens.d.cts","names":[],"sources":["../src/theme-tokens.ts"],"mappings":";;;;;;;;;;;KAUY,cAAc;;;;;;;;;;;UAeT;EACf,SAAS;EACT,aAAa;EACb,UAAU;;;;;UAMK;;;;;;;;;;EAUf,WAAW;;;;;;;;;;;;;;;;;;;;;;iBAuBG,oBACd,OAAO,mBAAmB,oBAC1B,UAAS;;;;;;;;;iBAiBK,YAAY,OAAO,aAAa,MAAM"}
1
+ {"version":3,"file":"theme-tokens.d.cts","sourceRoot":"","sources":["../../../../home/runner/work/ox-content/ox-content/npm/vite-plugin-ox-content/src/theme-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAKjD;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,EAC5C,OAAO,GAAE,0BAA+B,GACvC,MAAM,CAMR;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAgBzE"}
@@ -1,4 +1,3 @@
1
- //#region src/theme-tokens.d.ts
2
1
  /**
3
2
  * Free-form `--octc-*` custom properties for themes that need more than the
4
3
  * typed `colors` / `fonts` / `layout` fields.
@@ -9,7 +8,7 @@
9
8
  * accents, and surface textures purely through tokens, while a skin package
10
9
  * lays out geometry against those same tokens without knowing any color.
11
10
  */
12
- type ThemeTokens = Record<string, string>;
11
+ export type ThemeTokens = Record<string, string>;
13
12
  /**
14
13
  * The token-bearing shape of a theme.
15
14
  *
@@ -20,25 +19,25 @@ type ThemeTokens = Record<string, string>;
20
19
  * `ThemeConfig` — including the published `@ox-content/theme-color-*` and
21
20
  * `@ox-content/theme-*` packages — satisfies it.
22
21
  */
23
- interface ThemeTokenSource {
24
- tokens?: ThemeTokens;
25
- darkTokens?: ThemeTokens;
26
- extends?: ThemeTokenSource;
22
+ export interface ThemeTokenSource {
23
+ tokens?: ThemeTokens;
24
+ darkTokens?: ThemeTokens;
25
+ extends?: ThemeTokenSource;
27
26
  }
28
27
  /**
29
28
  * Options for {@link renderThemeTokenCss}.
30
29
  */
31
- interface RenderThemeTokenCssOptions {
32
- /**
33
- * Keeps only the tokens whose name passes the predicate. Names arrive without
34
- * the `--octc-` prefix, so `(name) => name.startsWith("syntax-")` reuses a
35
- * color scheme's highlighter palette without adopting its page colors,
36
- * typography, or layout policy.
37
- *
38
- * Filtering runs per layer, before merging, so a token a later layer would
39
- * have overridden is dropped along with the override.
40
- */
41
- include?: (name: string) => boolean;
30
+ export interface RenderThemeTokenCssOptions {
31
+ /**
32
+ * Keeps only the tokens whose name passes the predicate. Names arrive without
33
+ * the `--octc-` prefix, so `(name) => name.startsWith("syntax-")` reuses a
34
+ * color scheme's highlighter palette without adopting its page colors,
35
+ * typography, or layout policy.
36
+ *
37
+ * Filtering runs per layer, before merging, so a token a later layer would
38
+ * have overridden is dropped along with the override.
39
+ */
40
+ include?: (name: string) => boolean;
42
41
  }
43
42
  /**
44
43
  * Renders a theme's `--octc-*` tokens as a standalone stylesheet.
@@ -60,7 +59,7 @@ interface RenderThemeTokenCssOptions {
60
59
  * Layers compose left to right and each layer's `extends` chain is flattened
61
60
  * base-first, matching how `resolveTheme()` stacks a skin and a color scheme.
62
61
  */
63
- declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[], options?: RenderThemeTokenCssOptions): string;
62
+ export declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[], options?: RenderThemeTokenCssOptions): string;
64
63
  /**
65
64
  * Renders light and dark token records as the three selectors the SSG runtime
66
65
  * switches between: an explicit `[data-theme="dark"]` opt-in, the OS
@@ -69,7 +68,5 @@ declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[
69
68
  * Emitted after the typed color variables and before the theme's own `css`, so
70
69
  * a token can override a typed color and raw `css` can override a token.
71
70
  */
72
- declare function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string;
73
- //#endregion
74
- export { RenderThemeTokenCssOptions, ThemeTokenSource, ThemeTokens, renderThemeTokenCss, tokensToCss };
71
+ export declare function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string;
75
72
  //# sourceMappingURL=theme-tokens.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme-tokens.d.mts","names":[],"sources":["../src/theme-tokens.ts"],"mappings":";;;;;;;;;;;KAUY,cAAc;;;;;;;;;;;UAeT;EACf,SAAS;EACT,aAAa;EACb,UAAU;;;;;UAMK;;;;;;;;;;EAUf,WAAW;;;;;;;;;;;;;;;;;;;;;;iBAuBG,oBACd,OAAO,mBAAmB,oBAC1B,UAAS;;;;;;;;;iBAiBK,YAAY,OAAO,aAAa,MAAM"}
1
+ {"version":3,"file":"theme-tokens.d.mts","sourceRoot":"","sources":["../../../../home/runner/work/ox-content/ox-content/npm/vite-plugin-ox-content/src/theme-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAKjD;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,EAC5C,OAAO,GAAE,0BAA+B,GACvC,MAAM,CAMR;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAgBzE"}
@@ -0,0 +1,161 @@
1
+ // Generated from crates/ox_content_ssg/src/html/theme_transition_runtime.js.
2
+ // Run npm/vite-plugin-ox-content/scripts/build-theme-transition-client.mjs.
3
+
4
+ "use strict";
5
+ // Circular reveal for a same-document colour-scheme change.
6
+ //
7
+ // Prior art: @hooray's VitePress implementation, by way of @ryoppippi's
8
+ // svelte-fancy-darkmode, which this is a framework-neutral port of.
9
+ //
10
+ // This wraps a *same-document* theme mutation. It is unrelated to the
11
+ // cross-document `@view-transition` used for MPA navigation, and the CSS it
12
+ // depends on is scoped to `data-ox-theme-transition` so the two lifecycles
13
+ // cannot reach each other's snapshots.
14
+
15
+ const OX_THEME_TRANSITION_ATTR = "data-ox-theme-transition";
16
+ const OX_THEME_TRANSITION_SUPPRESS_ATTR = "data-ox-theme-transition-suppress";
17
+
18
+ let oxThemeTransitionCurrent = null;
19
+
20
+ function oxThemeTransitionRoot() {
21
+ return typeof document === "undefined" ? null : document.documentElement;
22
+ }
23
+
24
+ function oxThemeTransitionReducedMotion() {
25
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
26
+ }
27
+
28
+ function oxThemeTransitionSupported(root) {
29
+ return Boolean(
30
+ root &&
31
+ typeof document !== "undefined" &&
32
+ typeof document.startViewTransition === "function" &&
33
+ typeof root.animate === "function",
34
+ );
35
+ }
36
+
37
+ // A keyboard or programmatic activation still produces a click event, but with
38
+ // no meaningful coordinates: `detail` is 0 and clientX/clientY report 0. Only
39
+ // a real pointer press is trusted, so the reveal never starts from the corner
40
+ // of the screen when someone tabs to the button.
41
+ function oxThemeTransitionOrigin(event) {
42
+ const viewportCentre = {
43
+ x: (typeof innerWidth === "number" ? innerWidth : 0) / 2,
44
+ y: (typeof innerHeight === "number" ? innerHeight : 0) / 2,
45
+ };
46
+ if (!event) return viewportCentre;
47
+
48
+ const fromPointer =
49
+ typeof event.clientX === "number" &&
50
+ typeof event.clientY === "number" &&
51
+ (event.detail > 0 || (typeof event.pointerType === "string" && event.pointerType !== ""));
52
+ if (fromPointer) return { x: event.clientX, y: event.clientY };
53
+
54
+ const target = event.currentTarget ?? event.target;
55
+ if (target && typeof target.getBoundingClientRect === "function") {
56
+ const rect = target.getBoundingClientRect();
57
+ if (rect.width > 0 || rect.height > 0) {
58
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
59
+ }
60
+ }
61
+ return viewportCentre;
62
+ }
63
+
64
+ // The circle has to reach whichever viewport corner is furthest away, or the
65
+ // old theme stays visible in one corner when the animation ends.
66
+ function oxThemeTransitionRadius(origin) {
67
+ const width = typeof innerWidth === "number" ? innerWidth : 0;
68
+ const height = typeof innerHeight === "number" ? innerHeight : 0;
69
+ return Math.hypot(Math.max(origin.x, width - origin.x), Math.max(origin.y, height - origin.y));
70
+ }
71
+
72
+ // Element-level colour transitions would otherwise animate underneath the
73
+ // snapshot and bleed the old palette into the new one. They are suppressed
74
+ // only while the mutation happens, and restored as soon as both snapshots
75
+ // have been captured.
76
+ function oxThemeTransitionSuppress(root) {
77
+ root.setAttribute(OX_THEME_TRANSITION_SUPPRESS_ATTR, "");
78
+ return () => {
79
+ // Reading a layout property flushes the suppressed styles before they are
80
+ // allowed to animate again.
81
+ void root.offsetHeight;
82
+ root.removeAttribute(OX_THEME_TRANSITION_SUPPRESS_ATTR);
83
+ };
84
+ }
85
+
86
+ // eslint-disable-next-line no-unused-vars
87
+ function applyThemeTransition(options) {
88
+ const settings = options ?? {};
89
+ const apply = settings.apply;
90
+ if (typeof apply !== "function") return Promise.resolve();
91
+
92
+ const root = oxThemeTransitionRoot();
93
+ if (!oxThemeTransitionSupported(root) || oxThemeTransitionReducedMotion()) {
94
+ apply();
95
+ return Promise.resolve();
96
+ }
97
+
98
+ // A second toggle while one is still running would capture a half-animated
99
+ // snapshot. Settle the running one first; its cleanup is synchronous.
100
+ if (oxThemeTransitionCurrent) {
101
+ const running = oxThemeTransitionCurrent;
102
+ oxThemeTransitionCurrent = null;
103
+ if (typeof running.skipTransition === "function") running.skipTransition();
104
+ }
105
+
106
+ // Going dark expands the incoming snapshot over the outgoing one; going
107
+ // light shrinks the outgoing snapshot away to reveal the incoming one
108
+ // underneath. Either way the circle grows out of, or collapses into, the
109
+ // point the reader activated.
110
+ const expanding = settings.nextTheme !== "light";
111
+ const origin = oxThemeTransitionOrigin(settings.event);
112
+ const radius = oxThemeTransitionRadius(origin);
113
+ const duration = typeof settings.duration === "number" ? settings.duration : 420;
114
+ const easing = typeof settings.easing === "string" ? settings.easing : "ease-in-out";
115
+
116
+ root.setAttribute(OX_THEME_TRANSITION_ATTR, expanding ? "expand" : "shrink");
117
+
118
+ let restore = () => {};
119
+ const transition = document.startViewTransition(() => {
120
+ restore = oxThemeTransitionSuppress(root);
121
+ apply();
122
+ });
123
+ oxThemeTransitionCurrent = transition;
124
+
125
+ const clipFrom = `circle(0px at ${origin.x}px ${origin.y}px)`;
126
+ const clipTo = `circle(${radius}px at ${origin.x}px ${origin.y}px)`;
127
+
128
+ transition.ready.then(
129
+ () => {
130
+ restore();
131
+ root.animate(
132
+ { clipPath: expanding ? [clipFrom, clipTo] : [clipTo, clipFrom] },
133
+ {
134
+ duration,
135
+ easing,
136
+ pseudoElement: expanding ? "::view-transition-new(root)" : "::view-transition-old(root)",
137
+ },
138
+ );
139
+ },
140
+ () => {
141
+ // A skipped or unsupported transition still has to hand the suppressed
142
+ // styles back.
143
+ restore();
144
+ },
145
+ );
146
+
147
+ const settled = () => {
148
+ restore();
149
+ // A rapid second toggle skips this one, which lands here *after* the newer
150
+ // transition has already claimed the attribute. Only the transition still
151
+ // holding the slot is allowed to clear it.
152
+ if (oxThemeTransitionCurrent !== transition) return;
153
+ oxThemeTransitionCurrent = null;
154
+ root.removeAttribute(OX_THEME_TRANSITION_ATTR);
155
+ };
156
+ // `finished` rejects when a transition is skipped, which is a normal outcome
157
+ // here — both arms clean up and neither leaves an unhandled rejection.
158
+ return transition.finished.then(settled, settled);
159
+ }
160
+
161
+ module.exports = { applyThemeTransition };
@@ -0,0 +1,21 @@
1
+ export interface ThemeTransitionOptions {
2
+ /** Activation event. Supplies the reveal origin; omit for the viewport centre. */
3
+ event?: Event;
4
+ /** Theme being switched to. `"light"` collapses the circle, anything else grows it. */
5
+ nextTheme?: string;
6
+ /** Synchronous theme mutation, run while both snapshots are captured. */
7
+ apply: () => void;
8
+ /** Reveal duration in milliseconds. Defaults to 420. */
9
+ duration?: number;
10
+ /** Reveal easing. Defaults to `"ease-in-out"`. */
11
+ easing?: string;
12
+ }
13
+
14
+ /**
15
+ * Runs `apply` inside a circular view transition, or immediately when View
16
+ * Transitions are unavailable or the reader asked for reduced motion.
17
+ *
18
+ * Resolves once the transition has settled. A skipped transition resolves too,
19
+ * so the returned promise never rejects.
20
+ */
21
+ export declare function applyThemeTransition(options: ThemeTransitionOptions): Promise<void>;
@@ -0,0 +1,21 @@
1
+ export interface ThemeTransitionOptions {
2
+ /** Activation event. Supplies the reveal origin; omit for the viewport centre. */
3
+ event?: Event;
4
+ /** Theme being switched to. `"light"` collapses the circle, anything else grows it. */
5
+ nextTheme?: string;
6
+ /** Synchronous theme mutation, run while both snapshots are captured. */
7
+ apply: () => void;
8
+ /** Reveal duration in milliseconds. Defaults to 420. */
9
+ duration?: number;
10
+ /** Reveal easing. Defaults to `"ease-in-out"`. */
11
+ easing?: string;
12
+ }
13
+
14
+ /**
15
+ * Runs `apply` inside a circular view transition, or immediately when View
16
+ * Transitions are unavailable or the reader asked for reduced motion.
17
+ *
18
+ * Resolves once the transition has settled. A skipped transition resolves too,
19
+ * so the returned promise never rejects.
20
+ */
21
+ export declare function applyThemeTransition(options: ThemeTransitionOptions): Promise<void>;
@@ -0,0 +1,160 @@
1
+ // Generated from crates/ox_content_ssg/src/html/theme_transition_runtime.js.
2
+ // Run npm/vite-plugin-ox-content/scripts/build-theme-transition-client.mjs.
3
+
4
+ // Circular reveal for a same-document colour-scheme change.
5
+ //
6
+ // Prior art: @hooray's VitePress implementation, by way of @ryoppippi's
7
+ // svelte-fancy-darkmode, which this is a framework-neutral port of.
8
+ //
9
+ // This wraps a *same-document* theme mutation. It is unrelated to the
10
+ // cross-document `@view-transition` used for MPA navigation, and the CSS it
11
+ // depends on is scoped to `data-ox-theme-transition` so the two lifecycles
12
+ // cannot reach each other's snapshots.
13
+
14
+ const OX_THEME_TRANSITION_ATTR = "data-ox-theme-transition";
15
+ const OX_THEME_TRANSITION_SUPPRESS_ATTR = "data-ox-theme-transition-suppress";
16
+
17
+ let oxThemeTransitionCurrent = null;
18
+
19
+ function oxThemeTransitionRoot() {
20
+ return typeof document === "undefined" ? null : document.documentElement;
21
+ }
22
+
23
+ function oxThemeTransitionReducedMotion() {
24
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
25
+ }
26
+
27
+ function oxThemeTransitionSupported(root) {
28
+ return Boolean(
29
+ root &&
30
+ typeof document !== "undefined" &&
31
+ typeof document.startViewTransition === "function" &&
32
+ typeof root.animate === "function",
33
+ );
34
+ }
35
+
36
+ // A keyboard or programmatic activation still produces a click event, but with
37
+ // no meaningful coordinates: `detail` is 0 and clientX/clientY report 0. Only
38
+ // a real pointer press is trusted, so the reveal never starts from the corner
39
+ // of the screen when someone tabs to the button.
40
+ function oxThemeTransitionOrigin(event) {
41
+ const viewportCentre = {
42
+ x: (typeof innerWidth === "number" ? innerWidth : 0) / 2,
43
+ y: (typeof innerHeight === "number" ? innerHeight : 0) / 2,
44
+ };
45
+ if (!event) return viewportCentre;
46
+
47
+ const fromPointer =
48
+ typeof event.clientX === "number" &&
49
+ typeof event.clientY === "number" &&
50
+ (event.detail > 0 || (typeof event.pointerType === "string" && event.pointerType !== ""));
51
+ if (fromPointer) return { x: event.clientX, y: event.clientY };
52
+
53
+ const target = event.currentTarget ?? event.target;
54
+ if (target && typeof target.getBoundingClientRect === "function") {
55
+ const rect = target.getBoundingClientRect();
56
+ if (rect.width > 0 || rect.height > 0) {
57
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
58
+ }
59
+ }
60
+ return viewportCentre;
61
+ }
62
+
63
+ // The circle has to reach whichever viewport corner is furthest away, or the
64
+ // old theme stays visible in one corner when the animation ends.
65
+ function oxThemeTransitionRadius(origin) {
66
+ const width = typeof innerWidth === "number" ? innerWidth : 0;
67
+ const height = typeof innerHeight === "number" ? innerHeight : 0;
68
+ return Math.hypot(Math.max(origin.x, width - origin.x), Math.max(origin.y, height - origin.y));
69
+ }
70
+
71
+ // Element-level colour transitions would otherwise animate underneath the
72
+ // snapshot and bleed the old palette into the new one. They are suppressed
73
+ // only while the mutation happens, and restored as soon as both snapshots
74
+ // have been captured.
75
+ function oxThemeTransitionSuppress(root) {
76
+ root.setAttribute(OX_THEME_TRANSITION_SUPPRESS_ATTR, "");
77
+ return () => {
78
+ // Reading a layout property flushes the suppressed styles before they are
79
+ // allowed to animate again.
80
+ void root.offsetHeight;
81
+ root.removeAttribute(OX_THEME_TRANSITION_SUPPRESS_ATTR);
82
+ };
83
+ }
84
+
85
+ // eslint-disable-next-line no-unused-vars
86
+ function applyThemeTransition(options) {
87
+ const settings = options ?? {};
88
+ const apply = settings.apply;
89
+ if (typeof apply !== "function") return Promise.resolve();
90
+
91
+ const root = oxThemeTransitionRoot();
92
+ if (!oxThemeTransitionSupported(root) || oxThemeTransitionReducedMotion()) {
93
+ apply();
94
+ return Promise.resolve();
95
+ }
96
+
97
+ // A second toggle while one is still running would capture a half-animated
98
+ // snapshot. Settle the running one first; its cleanup is synchronous.
99
+ if (oxThemeTransitionCurrent) {
100
+ const running = oxThemeTransitionCurrent;
101
+ oxThemeTransitionCurrent = null;
102
+ if (typeof running.skipTransition === "function") running.skipTransition();
103
+ }
104
+
105
+ // Going dark expands the incoming snapshot over the outgoing one; going
106
+ // light shrinks the outgoing snapshot away to reveal the incoming one
107
+ // underneath. Either way the circle grows out of, or collapses into, the
108
+ // point the reader activated.
109
+ const expanding = settings.nextTheme !== "light";
110
+ const origin = oxThemeTransitionOrigin(settings.event);
111
+ const radius = oxThemeTransitionRadius(origin);
112
+ const duration = typeof settings.duration === "number" ? settings.duration : 420;
113
+ const easing = typeof settings.easing === "string" ? settings.easing : "ease-in-out";
114
+
115
+ root.setAttribute(OX_THEME_TRANSITION_ATTR, expanding ? "expand" : "shrink");
116
+
117
+ let restore = () => {};
118
+ const transition = document.startViewTransition(() => {
119
+ restore = oxThemeTransitionSuppress(root);
120
+ apply();
121
+ });
122
+ oxThemeTransitionCurrent = transition;
123
+
124
+ const clipFrom = `circle(0px at ${origin.x}px ${origin.y}px)`;
125
+ const clipTo = `circle(${radius}px at ${origin.x}px ${origin.y}px)`;
126
+
127
+ transition.ready.then(
128
+ () => {
129
+ restore();
130
+ root.animate(
131
+ { clipPath: expanding ? [clipFrom, clipTo] : [clipTo, clipFrom] },
132
+ {
133
+ duration,
134
+ easing,
135
+ pseudoElement: expanding ? "::view-transition-new(root)" : "::view-transition-old(root)",
136
+ },
137
+ );
138
+ },
139
+ () => {
140
+ // A skipped or unsupported transition still has to hand the suppressed
141
+ // styles back.
142
+ restore();
143
+ },
144
+ );
145
+
146
+ const settled = () => {
147
+ restore();
148
+ // A rapid second toggle skips this one, which lands here *after* the newer
149
+ // transition has already claimed the attribute. Only the transition still
150
+ // holding the slot is allowed to clear it.
151
+ if (oxThemeTransitionCurrent !== transition) return;
152
+ oxThemeTransitionCurrent = null;
153
+ root.removeAttribute(OX_THEME_TRANSITION_ATTR);
154
+ };
155
+ // `finished` rejects when a transition is skipped, which is a normal outcome
156
+ // here — both arms clean up and neither leaves an unhandled rejection.
157
+ return transition.finished.then(settled, settled);
158
+ }
159
+
160
+ export { applyThemeTransition };
@@ -0,0 +1,84 @@
1
+ // Generated from crates/ox_content_ssg/src/html/plugins/twitter.js.
2
+ // Run npm/vite-plugin-ox-content/scripts/build-twitter-client.mjs.
3
+
4
+ "use strict";
5
+ const TWEET_COPY_RESET_MS = 6000;
6
+ const oxTweetInitializedRoots = new WeakSet();
7
+ const oxTweetCopyResetTimers = new WeakMap();
8
+ const oxTweetHandledEvents = new WeakSet();
9
+
10
+ // eslint-disable-next-line no-unused-vars
11
+ function initTweetCards(rootInput, options) {
12
+ const fallbackDocument = typeof document === "undefined" ? undefined : document;
13
+ const root = rootInput ?? fallbackDocument;
14
+ if (!root || oxTweetInitializedRoots.has(root) || typeof root.addEventListener !== "function") {
15
+ return;
16
+ }
17
+
18
+ oxTweetInitializedRoots.add(root);
19
+ const copiedMs =
20
+ options && Number.isFinite(options.copiedMs)
21
+ ? Math.max(0, options.copiedMs)
22
+ : TWEET_COPY_RESET_MS;
23
+ const getClipboard = () =>
24
+ options && options.clipboard
25
+ ? options.clipboard
26
+ : typeof navigator === "undefined"
27
+ ? undefined
28
+ : navigator.clipboard;
29
+
30
+ root.addEventListener("click", (event) => {
31
+ if (oxTweetHandledEvents.has(event) || event.defaultPrevented) return;
32
+ const action = closestTweetCopyAction(event.target);
33
+ if (!action) return;
34
+
35
+ const url = action.getAttribute("data-ox-tweet-copy-url") || action.getAttribute("href");
36
+ const clipboard = getClipboard();
37
+ if (!url || !clipboard || typeof clipboard.writeText !== "function") return;
38
+
39
+ oxTweetHandledEvents.add(event);
40
+ event.preventDefault();
41
+ clipboard
42
+ .writeText(url)
43
+ .then(() => setTweetCopyState(action, "copied", copiedMs))
44
+ .catch(() => setTweetCopyState(action, "failed", copiedMs));
45
+ });
46
+ }
47
+
48
+ // eslint-disable-next-line no-unused-vars
49
+ const initTwitterCards = initTweetCards;
50
+
51
+ function closestTweetCopyAction(target) {
52
+ if (!target || typeof target.closest !== "function") return undefined;
53
+ const action = target.closest("[data-ox-tweet-copy]");
54
+ if (!action || typeof action.getAttribute !== "function") return undefined;
55
+ return action;
56
+ }
57
+
58
+ function setTweetCopyState(action, state, copiedMs) {
59
+ const previousTimer = oxTweetCopyResetTimers.get(action);
60
+ if (previousTimer) clearTimeout(previousTimer);
61
+
62
+ const label = state === "copied" ? "Copied!" : state === "failed" ? "Copy failed" : "Copy link";
63
+ const status =
64
+ typeof action.querySelector === "function"
65
+ ? action.querySelector("[data-ox-tweet-copy-status]")
66
+ : undefined;
67
+
68
+ if (state === "copied") action.setAttribute("data-ox-tweet-copied", "");
69
+ else action.removeAttribute("data-ox-tweet-copied");
70
+ action.setAttribute("aria-label", label);
71
+ action.setAttribute("title", label);
72
+ if (status) status.textContent = state ? label : "";
73
+
74
+ if (state) {
75
+ oxTweetCopyResetTimers.set(
76
+ action,
77
+ setTimeout(() => setTweetCopyState(action, undefined, copiedMs), copiedMs),
78
+ );
79
+ } else {
80
+ oxTweetCopyResetTimers.delete(action);
81
+ }
82
+ }
83
+
84
+ module.exports = { TWEET_COPY_RESET_MS, initTweetCards, initTwitterCards };
@@ -0,0 +1,11 @@
1
+ export type TweetCardsRoot = Document | Element;
2
+ export interface InitTweetCardsOptions {
3
+ clipboard?: Pick<Clipboard, "writeText">;
4
+ copiedMs?: number;
5
+ }
6
+ export declare const TWEET_COPY_RESET_MS = 6000;
7
+ export declare function initTweetCards(
8
+ root?: TweetCardsRoot,
9
+ options?: InitTweetCardsOptions,
10
+ ): void;
11
+ export declare const initTwitterCards: typeof initTweetCards;
@@ -0,0 +1,11 @@
1
+ export type TweetCardsRoot = Document | Element;
2
+ export interface InitTweetCardsOptions {
3
+ clipboard?: Pick<Clipboard, "writeText">;
4
+ copiedMs?: number;
5
+ }
6
+ export declare const TWEET_COPY_RESET_MS = 6000;
7
+ export declare function initTweetCards(
8
+ root?: TweetCardsRoot,
9
+ options?: InitTweetCardsOptions,
10
+ ): void;
11
+ export declare const initTwitterCards: typeof initTweetCards;