@ox-content/vite-plugin 3.0.0-alpha.8 → 3.0.0-beta.0
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.
- package/dist/index.cjs +8898 -2585
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2050 -221
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2050 -221
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +8839 -2531
- package/dist/index.mjs.map +1 -1
- package/dist/markdown-tables.cjs +70 -0
- package/dist/markdown-tables.cjs.map +1 -0
- package/dist/markdown-tables.d.cts +22 -0
- package/dist/markdown-tables.d.cts.map +1 -0
- package/dist/markdown-tables.d.mts +22 -0
- package/dist/markdown-tables.d.mts.map +1 -0
- package/dist/markdown-tables.mjs +66 -0
- package/dist/markdown-tables.mjs.map +1 -0
- package/dist/napi.cjs +44 -0
- package/dist/napi.cjs.map +1 -0
- package/dist/napi.mjs +34 -0
- package/dist/napi.mjs.map +1 -0
- package/dist/reader-chrome-client.cjs +103 -0
- package/dist/reader-chrome-client.d.cts +2 -0
- package/dist/reader-chrome-client.d.mts +2 -0
- package/dist/reader-chrome-client.mjs +102 -0
- package/dist/reader-chrome.cjs +109 -0
- package/dist/reader-chrome.cjs.map +1 -0
- package/dist/reader-chrome.d.cts +103 -0
- package/dist/reader-chrome.d.mts +103 -0
- package/dist/reader-chrome.mjs +98 -0
- package/dist/reader-chrome.mjs.map +1 -0
- package/dist/styles/all.css +15 -0
- package/dist/styles/citations.css +62 -0
- package/dist/styles/core.css +2261 -0
- package/dist/styles/github.css +262 -0
- package/dist/styles/graphviz.css +34 -0
- package/dist/styles/magic-links.css +33 -0
- package/dist/styles/markdown-tables.css +16 -0
- package/dist/styles/mermaid.css +151 -0
- package/dist/styles/not-by-ai.css +43 -0
- package/dist/styles/ogp.css +170 -0
- package/dist/styles/reader-chrome.css +163 -0
- package/dist/styles/social.css +678 -0
- package/dist/styles/tabs.css +210 -0
- package/dist/styles/theme-transition.css +37 -0
- package/dist/styles/twitter-full.css +598 -0
- package/dist/styles/youtube.css +87 -0
- package/dist/theme-tokens.cjs +95 -0
- package/dist/theme-tokens.cjs.map +1 -0
- package/dist/theme-tokens.d.cts +72 -0
- package/dist/theme-tokens.d.cts.map +1 -0
- package/dist/theme-tokens.d.mts +72 -0
- package/dist/theme-tokens.d.mts.map +1 -0
- package/dist/theme-tokens.mjs +91 -0
- package/dist/theme-tokens.mjs.map +1 -0
- package/dist/theme-transition-client.cjs +161 -0
- package/dist/theme-transition-client.d.cts +21 -0
- package/dist/theme-transition-client.d.mts +21 -0
- package/dist/theme-transition-client.mjs +160 -0
- package/dist/twitter-client.cjs +84 -0
- package/dist/twitter-client.d.cts +11 -0
- package/dist/twitter-client.d.mts +11 -0
- package/dist/twitter-client.mjs +83 -0
- package/dist/vitepress.cjs +614 -77
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.mjs +588 -65
- package/dist/vitepress.mjs.map +1 -1
- package/package.json +89 -4
- package/dist/api.cjs +0 -6563
- package/dist/api.cjs.map +0 -1
- package/dist/api.mjs +0 -6456
- package/dist/api.mjs.map +0 -1
- package/dist/interop.cjs +0 -31
- package/dist/interop.cjs.map +0 -1
- package/dist/interop.mjs +0 -26
- package/dist/interop.mjs.map +0 -1
- package/dist/tabs.cjs +0 -98
- package/dist/tabs.cjs.map +0 -1
- package/dist/tabs.mjs +0 -99
- package/dist/tabs.mjs.map +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderThemeTokenCss = renderThemeTokenCss;
|
|
4
|
+
exports.tokensToCss = tokensToCss;
|
|
5
|
+
const TOKEN_PREFIX = "--octc-";
|
|
6
|
+
const TOKEN_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
7
|
+
/**
|
|
8
|
+
* Renders a theme's `--octc-*` tokens as a standalone stylesheet.
|
|
9
|
+
*
|
|
10
|
+
* The built-in SSG emits these declarations itself, but `ssg.bare: true` and
|
|
11
|
+
* custom hosts render their own document — this is how they get the same
|
|
12
|
+
* tokens. The built-in highlighter emits `var(--octc-syntax-*)` references, so
|
|
13
|
+
* a bare host that wants only the highlighter palette can ask for it:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { renderThemeTokenCss } from "@ox-content/vite-plugin/theme-tokens";
|
|
17
|
+
* import { kanagawa } from "@ox-content/theme-color-kanagawa";
|
|
18
|
+
*
|
|
19
|
+
* const css = renderThemeTokenCss(kanagawa, {
|
|
20
|
+
* include: (name) => name.startsWith("syntax-"),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Layers compose left to right and each layer's `extends` chain is flattened
|
|
25
|
+
* base-first, matching how `resolveTheme()` stacks a skin and a color scheme.
|
|
26
|
+
*/
|
|
27
|
+
function renderThemeTokenCss(theme, options = {}) {
|
|
28
|
+
const layers = (Array.isArray(theme) ? theme : [theme]).flatMap(expandExtendsChain);
|
|
29
|
+
return tokensToCss(mergeTokens(layers, "tokens", options.include), mergeTokens(layers, "darkTokens", options.include));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Renders light and dark token records as the three selectors the SSG runtime
|
|
33
|
+
* switches between: an explicit `[data-theme="dark"]` opt-in, the OS
|
|
34
|
+
* `prefers-color-scheme` fallback, and the `:root` base.
|
|
35
|
+
*
|
|
36
|
+
* Emitted after the typed color variables and before the theme's own `css`, so
|
|
37
|
+
* a token can override a typed color and raw `css` can override a token.
|
|
38
|
+
*/
|
|
39
|
+
function tokensToCss(light, dark) {
|
|
40
|
+
const lightBody = declarations(light, " ");
|
|
41
|
+
const darkBody = declarations(dark, " ");
|
|
42
|
+
const blocks = [];
|
|
43
|
+
if (lightBody) {
|
|
44
|
+
blocks.push(`:root {\n${lightBody}\n}`);
|
|
45
|
+
}
|
|
46
|
+
if (darkBody) {
|
|
47
|
+
blocks.push(`[data-theme="dark"] {\n${darkBody}\n}`);
|
|
48
|
+
blocks.push(`@media (prefers-color-scheme: dark) {\n :root:not([data-theme="light"]) {\n${declarations(dark, " ")}\n }\n}`);
|
|
49
|
+
}
|
|
50
|
+
return blocks.join("\n");
|
|
51
|
+
}
|
|
52
|
+
function declarations(tokens, indent) {
|
|
53
|
+
return Object.entries(tokens)
|
|
54
|
+
.filter(([, value]) => value !== undefined && value !== "")
|
|
55
|
+
.map(([name, value]) => `${indent}${TOKEN_PREFIX}${assertTokenName(name)}: ${value};`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
}
|
|
58
|
+
function assertTokenName(name) {
|
|
59
|
+
// Token names land verbatim inside a declaration block, so a stray `:` or `}`
|
|
60
|
+
// would silently break every rule after it. Fail the build with the offending
|
|
61
|
+
// key instead of shipping a corrupt stylesheet.
|
|
62
|
+
if (!TOKEN_NAME_PATTERN.test(name)) {
|
|
63
|
+
throw new Error(`Invalid theme token name: ${JSON.stringify(name)}. ` +
|
|
64
|
+
`Token names are lowercase kebab-case without the "${TOKEN_PREFIX}" prefix (e.g. "surface-glass").`);
|
|
65
|
+
}
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
function mergeTokens(layers, field, include) {
|
|
69
|
+
const merged = {};
|
|
70
|
+
for (const layer of layers) {
|
|
71
|
+
for (const [name, value] of Object.entries(layer[field] ?? {})) {
|
|
72
|
+
if (!include || include(name)) {
|
|
73
|
+
merged[name] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return merged;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Flattens one layer's `extends` chain into base-first order, mirroring the
|
|
81
|
+
* SSG's own resolution. The `seen` guard keeps a theme that extends itself (or
|
|
82
|
+
* forms a cycle across two packages) from hanging the caller.
|
|
83
|
+
*/
|
|
84
|
+
function expandExtendsChain(theme) {
|
|
85
|
+
const chain = [];
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
let current = theme;
|
|
88
|
+
while (current && !seen.has(current)) {
|
|
89
|
+
seen.add(current);
|
|
90
|
+
chain.unshift(current);
|
|
91
|
+
current = current.extends;
|
|
92
|
+
}
|
|
93
|
+
return chain;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=theme-tokens.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-tokens.cjs","sourceRoot":"","sources":["../../../../home/runner/work/ox-content/ox-content/npm/vite-plugin-ox-content/src/theme-tokens.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Free-form `--octc-*` custom properties for themes that need more than the\n * typed `colors` / `fonts` / `layout` fields.\n *\n * Keys are written **without** the `--octc-` prefix, so `\"surface-glass\"`\n * becomes `--octc-surface-glass`. This is the seam that keeps the two theme\n * axes independent: a color package can restyle code-block line markers, brand\n * accents, and surface textures purely through tokens, while a skin package\n * lays out geometry against those same tokens without knowing any color.\n */\nexport type ThemeTokens = Record<string, string>;\n\nconst TOKEN_PREFIX = \"--octc-\";\nconst TOKEN_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;\n\n/**\n * The token-bearing shape of a theme.\n *\n * Declared structurally instead of importing `ThemeConfig` so this module keeps\n * an empty import graph: `@ox-content/vite-plugin/theme-tokens` has to be\n * loadable by a bare (`ssg.bare: true`) or custom host that never pulls in the\n * Vite plugin, the SSG, the native binding, or a filesystem API. Every\n * `ThemeConfig` — including the published `@ox-content/theme-color-*` and\n * `@ox-content/theme-*` packages — satisfies it.\n */\nexport interface ThemeTokenSource {\n tokens?: ThemeTokens;\n darkTokens?: ThemeTokens;\n extends?: ThemeTokenSource;\n}\n\n/**\n * Options for {@link renderThemeTokenCss}.\n */\nexport interface RenderThemeTokenCssOptions {\n /**\n * Keeps only the tokens whose name passes the predicate. Names arrive without\n * the `--octc-` prefix, so `(name) => name.startsWith(\"syntax-\")` reuses a\n * color scheme's highlighter palette without adopting its page colors,\n * typography, or layout policy.\n *\n * Filtering runs per layer, before merging, so a token a later layer would\n * have overridden is dropped along with the override.\n */\n include?: (name: string) => boolean;\n}\n\n/**\n * Renders a theme's `--octc-*` tokens as a standalone stylesheet.\n *\n * The built-in SSG emits these declarations itself, but `ssg.bare: true` and\n * custom hosts render their own document — this is how they get the same\n * tokens. The built-in highlighter emits `var(--octc-syntax-*)` references, so\n * a bare host that wants only the highlighter palette can ask for it:\n *\n * ```ts\n * import { renderThemeTokenCss } from \"@ox-content/vite-plugin/theme-tokens\";\n * import { kanagawa } from \"@ox-content/theme-color-kanagawa\";\n *\n * const css = renderThemeTokenCss(kanagawa, {\n * include: (name) => name.startsWith(\"syntax-\"),\n * });\n * ```\n *\n * Layers compose left to right and each layer's `extends` chain is flattened\n * base-first, matching how `resolveTheme()` stacks a skin and a color scheme.\n */\nexport function renderThemeTokenCss(\n theme: ThemeTokenSource | ThemeTokenSource[],\n options: RenderThemeTokenCssOptions = {},\n): string {\n const layers = (Array.isArray(theme) ? theme : [theme]).flatMap(expandExtendsChain);\n return tokensToCss(\n mergeTokens(layers, \"tokens\", options.include),\n mergeTokens(layers, \"darkTokens\", options.include),\n );\n}\n\n/**\n * Renders light and dark token records as the three selectors the SSG runtime\n * switches between: an explicit `[data-theme=\"dark\"]` opt-in, the OS\n * `prefers-color-scheme` fallback, and the `:root` base.\n *\n * Emitted after the typed color variables and before the theme's own `css`, so\n * a token can override a typed color and raw `css` can override a token.\n */\nexport function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string {\n const lightBody = declarations(light, \" \");\n const darkBody = declarations(dark, \" \");\n const blocks: string[] = [];\n\n if (lightBody) {\n blocks.push(`:root {\\n${lightBody}\\n}`);\n }\n if (darkBody) {\n blocks.push(`[data-theme=\"dark\"] {\\n${darkBody}\\n}`);\n blocks.push(\n `@media (prefers-color-scheme: dark) {\\n :root:not([data-theme=\"light\"]) {\\n${declarations(dark, \" \")}\\n }\\n}`,\n );\n }\n\n return blocks.join(\"\\n\");\n}\n\nfunction declarations(tokens: ThemeTokens, indent: string): string {\n return Object.entries(tokens)\n .filter(([, value]) => value !== undefined && value !== \"\")\n .map(([name, value]) => `${indent}${TOKEN_PREFIX}${assertTokenName(name)}: ${value};`)\n .join(\"\\n\");\n}\n\nfunction assertTokenName(name: string): string {\n // Token names land verbatim inside a declaration block, so a stray `:` or `}`\n // would silently break every rule after it. Fail the build with the offending\n // key instead of shipping a corrupt stylesheet.\n if (!TOKEN_NAME_PATTERN.test(name)) {\n throw new Error(\n `Invalid theme token name: ${JSON.stringify(name)}. ` +\n `Token names are lowercase kebab-case without the \"${TOKEN_PREFIX}\" prefix (e.g. \"surface-glass\").`,\n );\n }\n return name;\n}\n\nfunction mergeTokens(\n layers: ThemeTokenSource[],\n field: \"tokens\" | \"darkTokens\",\n include?: (name: string) => boolean,\n): ThemeTokens {\n const merged: ThemeTokens = {};\n\n for (const layer of layers) {\n for (const [name, value] of Object.entries(layer[field] ?? {})) {\n if (!include || include(name)) {\n merged[name] = value;\n }\n }\n }\n\n return merged;\n}\n\n/**\n * Flattens one layer's `extends` chain into base-first order, mirroring the\n * SSG's own resolution. The `seen` guard keeps a theme that extends itself (or\n * forms a cycle across two packages) from hanging the caller.\n */\nfunction expandExtendsChain(theme: ThemeTokenSource): ThemeTokenSource[] {\n const chain: ThemeTokenSource[] = [];\n const seen = new Set<ThemeTokenSource>();\n let current: ThemeTokenSource | undefined = theme;\n\n while (current && !seen.has(current)) {\n seen.add(current);\n chain.unshift(current);\n current = current.extends;\n }\n\n return chain;\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free-form `--octc-*` custom properties for themes that need more than the
|
|
3
|
+
* typed `colors` / `fonts` / `layout` fields.
|
|
4
|
+
*
|
|
5
|
+
* Keys are written **without** the `--octc-` prefix, so `"surface-glass"`
|
|
6
|
+
* becomes `--octc-surface-glass`. This is the seam that keeps the two theme
|
|
7
|
+
* axes independent: a color package can restyle code-block line markers, brand
|
|
8
|
+
* accents, and surface textures purely through tokens, while a skin package
|
|
9
|
+
* lays out geometry against those same tokens without knowing any color.
|
|
10
|
+
*/
|
|
11
|
+
export type ThemeTokens = Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* The token-bearing shape of a theme.
|
|
14
|
+
*
|
|
15
|
+
* Declared structurally instead of importing `ThemeConfig` so this module keeps
|
|
16
|
+
* an empty import graph: `@ox-content/vite-plugin/theme-tokens` has to be
|
|
17
|
+
* loadable by a bare (`ssg.bare: true`) or custom host that never pulls in the
|
|
18
|
+
* Vite plugin, the SSG, the native binding, or a filesystem API. Every
|
|
19
|
+
* `ThemeConfig` — including the published `@ox-content/theme-color-*` and
|
|
20
|
+
* `@ox-content/theme-*` packages — satisfies it.
|
|
21
|
+
*/
|
|
22
|
+
export interface ThemeTokenSource {
|
|
23
|
+
tokens?: ThemeTokens;
|
|
24
|
+
darkTokens?: ThemeTokens;
|
|
25
|
+
extends?: ThemeTokenSource;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for {@link renderThemeTokenCss}.
|
|
29
|
+
*/
|
|
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;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Renders a theme's `--octc-*` tokens as a standalone stylesheet.
|
|
44
|
+
*
|
|
45
|
+
* The built-in SSG emits these declarations itself, but `ssg.bare: true` and
|
|
46
|
+
* custom hosts render their own document — this is how they get the same
|
|
47
|
+
* tokens. The built-in highlighter emits `var(--octc-syntax-*)` references, so
|
|
48
|
+
* a bare host that wants only the highlighter palette can ask for it:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { renderThemeTokenCss } from "@ox-content/vite-plugin/theme-tokens";
|
|
52
|
+
* import { kanagawa } from "@ox-content/theme-color-kanagawa";
|
|
53
|
+
*
|
|
54
|
+
* const css = renderThemeTokenCss(kanagawa, {
|
|
55
|
+
* include: (name) => name.startsWith("syntax-"),
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* Layers compose left to right and each layer's `extends` chain is flattened
|
|
60
|
+
* base-first, matching how `resolveTheme()` stacks a skin and a color scheme.
|
|
61
|
+
*/
|
|
62
|
+
export declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[], options?: RenderThemeTokenCssOptions): string;
|
|
63
|
+
/**
|
|
64
|
+
* Renders light and dark token records as the three selectors the SSG runtime
|
|
65
|
+
* switches between: an explicit `[data-theme="dark"]` opt-in, the OS
|
|
66
|
+
* `prefers-color-scheme` fallback, and the `:root` base.
|
|
67
|
+
*
|
|
68
|
+
* Emitted after the typed color variables and before the theme's own `css`, so
|
|
69
|
+
* a token can override a typed color and raw `css` can override a token.
|
|
70
|
+
*/
|
|
71
|
+
export declare function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string;
|
|
72
|
+
//# sourceMappingURL=theme-tokens.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free-form `--octc-*` custom properties for themes that need more than the
|
|
3
|
+
* typed `colors` / `fonts` / `layout` fields.
|
|
4
|
+
*
|
|
5
|
+
* Keys are written **without** the `--octc-` prefix, so `"surface-glass"`
|
|
6
|
+
* becomes `--octc-surface-glass`. This is the seam that keeps the two theme
|
|
7
|
+
* axes independent: a color package can restyle code-block line markers, brand
|
|
8
|
+
* accents, and surface textures purely through tokens, while a skin package
|
|
9
|
+
* lays out geometry against those same tokens without knowing any color.
|
|
10
|
+
*/
|
|
11
|
+
export type ThemeTokens = Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* The token-bearing shape of a theme.
|
|
14
|
+
*
|
|
15
|
+
* Declared structurally instead of importing `ThemeConfig` so this module keeps
|
|
16
|
+
* an empty import graph: `@ox-content/vite-plugin/theme-tokens` has to be
|
|
17
|
+
* loadable by a bare (`ssg.bare: true`) or custom host that never pulls in the
|
|
18
|
+
* Vite plugin, the SSG, the native binding, or a filesystem API. Every
|
|
19
|
+
* `ThemeConfig` — including the published `@ox-content/theme-color-*` and
|
|
20
|
+
* `@ox-content/theme-*` packages — satisfies it.
|
|
21
|
+
*/
|
|
22
|
+
export interface ThemeTokenSource {
|
|
23
|
+
tokens?: ThemeTokens;
|
|
24
|
+
darkTokens?: ThemeTokens;
|
|
25
|
+
extends?: ThemeTokenSource;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for {@link renderThemeTokenCss}.
|
|
29
|
+
*/
|
|
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;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Renders a theme's `--octc-*` tokens as a standalone stylesheet.
|
|
44
|
+
*
|
|
45
|
+
* The built-in SSG emits these declarations itself, but `ssg.bare: true` and
|
|
46
|
+
* custom hosts render their own document — this is how they get the same
|
|
47
|
+
* tokens. The built-in highlighter emits `var(--octc-syntax-*)` references, so
|
|
48
|
+
* a bare host that wants only the highlighter palette can ask for it:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { renderThemeTokenCss } from "@ox-content/vite-plugin/theme-tokens";
|
|
52
|
+
* import { kanagawa } from "@ox-content/theme-color-kanagawa";
|
|
53
|
+
*
|
|
54
|
+
* const css = renderThemeTokenCss(kanagawa, {
|
|
55
|
+
* include: (name) => name.startsWith("syntax-"),
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* Layers compose left to right and each layer's `extends` chain is flattened
|
|
60
|
+
* base-first, matching how `resolveTheme()` stacks a skin and a color scheme.
|
|
61
|
+
*/
|
|
62
|
+
export declare function renderThemeTokenCss(theme: ThemeTokenSource | ThemeTokenSource[], options?: RenderThemeTokenCssOptions): string;
|
|
63
|
+
/**
|
|
64
|
+
* Renders light and dark token records as the three selectors the SSG runtime
|
|
65
|
+
* switches between: an explicit `[data-theme="dark"]` opt-in, the OS
|
|
66
|
+
* `prefers-color-scheme` fallback, and the `:root` base.
|
|
67
|
+
*
|
|
68
|
+
* Emitted after the typed color variables and before the theme's own `css`, so
|
|
69
|
+
* a token can override a typed color and raw `css` can override a token.
|
|
70
|
+
*/
|
|
71
|
+
export declare function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string;
|
|
72
|
+
//# sourceMappingURL=theme-tokens.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
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,91 @@
|
|
|
1
|
+
const TOKEN_PREFIX = "--octc-";
|
|
2
|
+
const TOKEN_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
3
|
+
/**
|
|
4
|
+
* Renders a theme's `--octc-*` tokens as a standalone stylesheet.
|
|
5
|
+
*
|
|
6
|
+
* The built-in SSG emits these declarations itself, but `ssg.bare: true` and
|
|
7
|
+
* custom hosts render their own document — this is how they get the same
|
|
8
|
+
* tokens. The built-in highlighter emits `var(--octc-syntax-*)` references, so
|
|
9
|
+
* a bare host that wants only the highlighter palette can ask for it:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { renderThemeTokenCss } from "@ox-content/vite-plugin/theme-tokens";
|
|
13
|
+
* import { kanagawa } from "@ox-content/theme-color-kanagawa";
|
|
14
|
+
*
|
|
15
|
+
* const css = renderThemeTokenCss(kanagawa, {
|
|
16
|
+
* include: (name) => name.startsWith("syntax-"),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Layers compose left to right and each layer's `extends` chain is flattened
|
|
21
|
+
* base-first, matching how `resolveTheme()` stacks a skin and a color scheme.
|
|
22
|
+
*/
|
|
23
|
+
export function renderThemeTokenCss(theme, options = {}) {
|
|
24
|
+
const layers = (Array.isArray(theme) ? theme : [theme]).flatMap(expandExtendsChain);
|
|
25
|
+
return tokensToCss(mergeTokens(layers, "tokens", options.include), mergeTokens(layers, "darkTokens", options.include));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Renders light and dark token records as the three selectors the SSG runtime
|
|
29
|
+
* switches between: an explicit `[data-theme="dark"]` opt-in, the OS
|
|
30
|
+
* `prefers-color-scheme` fallback, and the `:root` base.
|
|
31
|
+
*
|
|
32
|
+
* Emitted after the typed color variables and before the theme's own `css`, so
|
|
33
|
+
* a token can override a typed color and raw `css` can override a token.
|
|
34
|
+
*/
|
|
35
|
+
export function tokensToCss(light, dark) {
|
|
36
|
+
const lightBody = declarations(light, " ");
|
|
37
|
+
const darkBody = declarations(dark, " ");
|
|
38
|
+
const blocks = [];
|
|
39
|
+
if (lightBody) {
|
|
40
|
+
blocks.push(`:root {\n${lightBody}\n}`);
|
|
41
|
+
}
|
|
42
|
+
if (darkBody) {
|
|
43
|
+
blocks.push(`[data-theme="dark"] {\n${darkBody}\n}`);
|
|
44
|
+
blocks.push(`@media (prefers-color-scheme: dark) {\n :root:not([data-theme="light"]) {\n${declarations(dark, " ")}\n }\n}`);
|
|
45
|
+
}
|
|
46
|
+
return blocks.join("\n");
|
|
47
|
+
}
|
|
48
|
+
function declarations(tokens, indent) {
|
|
49
|
+
return Object.entries(tokens)
|
|
50
|
+
.filter(([, value]) => value !== undefined && value !== "")
|
|
51
|
+
.map(([name, value]) => `${indent}${TOKEN_PREFIX}${assertTokenName(name)}: ${value};`)
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
54
|
+
function assertTokenName(name) {
|
|
55
|
+
// Token names land verbatim inside a declaration block, so a stray `:` or `}`
|
|
56
|
+
// would silently break every rule after it. Fail the build with the offending
|
|
57
|
+
// key instead of shipping a corrupt stylesheet.
|
|
58
|
+
if (!TOKEN_NAME_PATTERN.test(name)) {
|
|
59
|
+
throw new Error(`Invalid theme token name: ${JSON.stringify(name)}. ` +
|
|
60
|
+
`Token names are lowercase kebab-case without the "${TOKEN_PREFIX}" prefix (e.g. "surface-glass").`);
|
|
61
|
+
}
|
|
62
|
+
return name;
|
|
63
|
+
}
|
|
64
|
+
function mergeTokens(layers, field, include) {
|
|
65
|
+
const merged = {};
|
|
66
|
+
for (const layer of layers) {
|
|
67
|
+
for (const [name, value] of Object.entries(layer[field] ?? {})) {
|
|
68
|
+
if (!include || include(name)) {
|
|
69
|
+
merged[name] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Flattens one layer's `extends` chain into base-first order, mirroring the
|
|
77
|
+
* SSG's own resolution. The `seen` guard keeps a theme that extends itself (or
|
|
78
|
+
* forms a cycle across two packages) from hanging the caller.
|
|
79
|
+
*/
|
|
80
|
+
function expandExtendsChain(theme) {
|
|
81
|
+
const chain = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
let current = theme;
|
|
84
|
+
while (current && !seen.has(current)) {
|
|
85
|
+
seen.add(current);
|
|
86
|
+
chain.unshift(current);
|
|
87
|
+
current = current.extends;
|
|
88
|
+
}
|
|
89
|
+
return chain;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=theme-tokens.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-tokens.mjs","sourceRoot":"","sources":["../../../../home/runner/work/ox-content/ox-content/npm/vite-plugin-ox-content/src/theme-tokens.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Free-form `--octc-*` custom properties for themes that need more than the\n * typed `colors` / `fonts` / `layout` fields.\n *\n * Keys are written **without** the `--octc-` prefix, so `\"surface-glass\"`\n * becomes `--octc-surface-glass`. This is the seam that keeps the two theme\n * axes independent: a color package can restyle code-block line markers, brand\n * accents, and surface textures purely through tokens, while a skin package\n * lays out geometry against those same tokens without knowing any color.\n */\nexport type ThemeTokens = Record<string, string>;\n\nconst TOKEN_PREFIX = \"--octc-\";\nconst TOKEN_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;\n\n/**\n * The token-bearing shape of a theme.\n *\n * Declared structurally instead of importing `ThemeConfig` so this module keeps\n * an empty import graph: `@ox-content/vite-plugin/theme-tokens` has to be\n * loadable by a bare (`ssg.bare: true`) or custom host that never pulls in the\n * Vite plugin, the SSG, the native binding, or a filesystem API. Every\n * `ThemeConfig` — including the published `@ox-content/theme-color-*` and\n * `@ox-content/theme-*` packages — satisfies it.\n */\nexport interface ThemeTokenSource {\n tokens?: ThemeTokens;\n darkTokens?: ThemeTokens;\n extends?: ThemeTokenSource;\n}\n\n/**\n * Options for {@link renderThemeTokenCss}.\n */\nexport interface RenderThemeTokenCssOptions {\n /**\n * Keeps only the tokens whose name passes the predicate. Names arrive without\n * the `--octc-` prefix, so `(name) => name.startsWith(\"syntax-\")` reuses a\n * color scheme's highlighter palette without adopting its page colors,\n * typography, or layout policy.\n *\n * Filtering runs per layer, before merging, so a token a later layer would\n * have overridden is dropped along with the override.\n */\n include?: (name: string) => boolean;\n}\n\n/**\n * Renders a theme's `--octc-*` tokens as a standalone stylesheet.\n *\n * The built-in SSG emits these declarations itself, but `ssg.bare: true` and\n * custom hosts render their own document — this is how they get the same\n * tokens. The built-in highlighter emits `var(--octc-syntax-*)` references, so\n * a bare host that wants only the highlighter palette can ask for it:\n *\n * ```ts\n * import { renderThemeTokenCss } from \"@ox-content/vite-plugin/theme-tokens\";\n * import { kanagawa } from \"@ox-content/theme-color-kanagawa\";\n *\n * const css = renderThemeTokenCss(kanagawa, {\n * include: (name) => name.startsWith(\"syntax-\"),\n * });\n * ```\n *\n * Layers compose left to right and each layer's `extends` chain is flattened\n * base-first, matching how `resolveTheme()` stacks a skin and a color scheme.\n */\nexport function renderThemeTokenCss(\n theme: ThemeTokenSource | ThemeTokenSource[],\n options: RenderThemeTokenCssOptions = {},\n): string {\n const layers = (Array.isArray(theme) ? theme : [theme]).flatMap(expandExtendsChain);\n return tokensToCss(\n mergeTokens(layers, \"tokens\", options.include),\n mergeTokens(layers, \"darkTokens\", options.include),\n );\n}\n\n/**\n * Renders light and dark token records as the three selectors the SSG runtime\n * switches between: an explicit `[data-theme=\"dark\"]` opt-in, the OS\n * `prefers-color-scheme` fallback, and the `:root` base.\n *\n * Emitted after the typed color variables and before the theme's own `css`, so\n * a token can override a typed color and raw `css` can override a token.\n */\nexport function tokensToCss(light: ThemeTokens, dark: ThemeTokens): string {\n const lightBody = declarations(light, \" \");\n const darkBody = declarations(dark, \" \");\n const blocks: string[] = [];\n\n if (lightBody) {\n blocks.push(`:root {\\n${lightBody}\\n}`);\n }\n if (darkBody) {\n blocks.push(`[data-theme=\"dark\"] {\\n${darkBody}\\n}`);\n blocks.push(\n `@media (prefers-color-scheme: dark) {\\n :root:not([data-theme=\"light\"]) {\\n${declarations(dark, \" \")}\\n }\\n}`,\n );\n }\n\n return blocks.join(\"\\n\");\n}\n\nfunction declarations(tokens: ThemeTokens, indent: string): string {\n return Object.entries(tokens)\n .filter(([, value]) => value !== undefined && value !== \"\")\n .map(([name, value]) => `${indent}${TOKEN_PREFIX}${assertTokenName(name)}: ${value};`)\n .join(\"\\n\");\n}\n\nfunction assertTokenName(name: string): string {\n // Token names land verbatim inside a declaration block, so a stray `:` or `}`\n // would silently break every rule after it. Fail the build with the offending\n // key instead of shipping a corrupt stylesheet.\n if (!TOKEN_NAME_PATTERN.test(name)) {\n throw new Error(\n `Invalid theme token name: ${JSON.stringify(name)}. ` +\n `Token names are lowercase kebab-case without the \"${TOKEN_PREFIX}\" prefix (e.g. \"surface-glass\").`,\n );\n }\n return name;\n}\n\nfunction mergeTokens(\n layers: ThemeTokenSource[],\n field: \"tokens\" | \"darkTokens\",\n include?: (name: string) => boolean,\n): ThemeTokens {\n const merged: ThemeTokens = {};\n\n for (const layer of layers) {\n for (const [name, value] of Object.entries(layer[field] ?? {})) {\n if (!include || include(name)) {\n merged[name] = value;\n }\n }\n }\n\n return merged;\n}\n\n/**\n * Flattens one layer's `extends` chain into base-first order, mirroring the\n * SSG's own resolution. The `seen` guard keeps a theme that extends itself (or\n * forms a cycle across two packages) from hanging the caller.\n */\nfunction expandExtendsChain(theme: ThemeTokenSource): ThemeTokenSource[] {\n const chain: ThemeTokenSource[] = [];\n const seen = new Set<ThemeTokenSource>();\n let current: ThemeTokenSource | undefined = theme;\n\n while (current && !seen.has(current)) {\n seen.add(current);\n chain.unshift(current);\n current = current.extends;\n }\n\n return chain;\n}\n"]}
|
|
@@ -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>;
|