@adea-ai/ui 0.128.1 → 0.128.3
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/components/theme/appearance-panel.js +1 -1
- package/dist/components/theme/index.js +1 -1
- package/dist/components/theme/lazy.d.ts +15 -0
- package/dist/components/theme/lazy.d.ts.map +1 -0
- package/dist/components/theme/lazy.js +9 -0
- package/dist/components/theme/theme-catalogue-source.d.ts +15 -0
- package/dist/components/theme/theme-catalogue-source.d.ts.map +1 -0
- package/dist/components/theme/theme-catalogue-source.js +51 -0
- package/dist/components/theme/theme-catalogue-source.js.map +1 -0
- package/dist/components/theme/theme-document-core.d.ts +123 -0
- package/dist/components/theme/theme-document-core.d.ts.map +1 -0
- package/dist/components/theme/{theme-document.js → theme-document-core.js} +12 -12
- package/dist/components/theme/theme-document-core.js.map +1 -0
- package/dist/components/theme/theme-document.d.ts +8 -107
- package/dist/components/theme/theme-document.d.ts.map +1 -1
- package/dist/components/theme/theme-persistence.d.ts +1 -1
- package/dist/components/theme/theme-persistence.d.ts.map +1 -1
- package/dist/components/theme/theme-persistence.js +9 -6
- package/dist/components/theme/theme-persistence.js.map +1 -1
- package/dist/components/theme/theme-provider-core.d.ts +19 -0
- package/dist/components/theme/theme-provider-core.d.ts.map +1 -0
- package/dist/components/theme/theme-provider-core.js +28 -0
- package/dist/components/theme/theme-provider-core.js.map +1 -0
- package/dist/components/theme/theme-provider-lazy.d.ts +28 -0
- package/dist/components/theme/theme-provider-lazy.d.ts.map +1 -0
- package/dist/components/theme/theme-provider-lazy.js +36 -0
- package/dist/components/theme/theme-provider-lazy.js.map +1 -0
- package/dist/components/theme/theme-provider-value.d.ts +175 -0
- package/dist/components/theme/theme-provider-value.d.ts.map +1 -0
- package/dist/components/theme/theme-provider-value.js +200 -0
- package/dist/components/theme/theme-provider-value.js.map +1 -0
- package/dist/components/theme/theme-provider.d.ts +11 -136
- package/dist/components/theme/theme-provider.d.ts.map +1 -1
- package/dist/components/theme/theme-provider.js +27 -149
- package/dist/components/theme/theme-provider.js.map +1 -1
- package/dist/components/theme/theme-script.js +4 -3
- package/dist/components/theme/theme-script.js.map +1 -1
- package/dist/components/theme/theme-state-core.d.ts +248 -0
- package/dist/components/theme/theme-state-core.d.ts.map +1 -0
- package/dist/components/theme/{theme-state.js → theme-state-core.js} +8 -7
- package/dist/components/theme/theme-state-core.js.map +1 -0
- package/dist/components/theme/theme-state.d.ts +8 -214
- package/dist/components/theme/theme-state.d.ts.map +1 -1
- package/dist/components/theme/theme-toggle.js.map +1 -1
- package/dist/index.js +7 -4
- package/dist/lib/accent-presets.d.ts +26 -0
- package/dist/lib/accent-presets.d.ts.map +1 -0
- package/dist/lib/accent-presets.js +11 -0
- package/dist/lib/accent-presets.js.map +1 -0
- package/dist/lib/accent-roles.js +94 -9
- package/dist/lib/accent-roles.js.map +1 -1
- package/dist/lib/font-zoom.js +4 -2
- package/dist/lib/font-zoom.js.map +1 -1
- package/dist/lib/overlay.js +3 -1
- package/dist/lib/overlay.js.map +1 -1
- package/dist/lib/theme-catalogue.d.ts +3 -0
- package/dist/lib/theme-catalogue.d.ts.map +1 -0
- package/dist/lib/theme-catalogue.js +22 -0
- package/dist/lib/theme-catalogue.js.map +1 -0
- package/dist/lib/theme-css.d.ts +43 -0
- package/dist/lib/theme-css.d.ts.map +1 -0
- package/dist/lib/theme-css.js +167 -0
- package/dist/lib/theme-css.js.map +1 -0
- package/dist/lib/theme-defaults.d.ts +46 -0
- package/dist/lib/theme-defaults.d.ts.map +1 -0
- package/dist/lib/theme-defaults.generated.d.ts +13 -0
- package/dist/lib/theme-defaults.generated.d.ts.map +1 -0
- package/dist/lib/theme-defaults.generated.js +318 -0
- package/dist/lib/theme-defaults.generated.js.map +1 -0
- package/dist/lib/theme-defaults.js +47 -0
- package/dist/lib/theme-defaults.js.map +1 -0
- package/dist/lib/theme-engine-light.d.ts +3 -0
- package/dist/lib/theme-engine-light.d.ts.map +1 -0
- package/dist/lib/theme-engine-light.js +75 -0
- package/dist/lib/theme-engine-light.js.map +1 -0
- package/dist/lib/theme-engine.d.ts +69 -0
- package/dist/lib/theme-engine.d.ts.map +1 -0
- package/dist/lib/theme-engine.js +33 -0
- package/dist/lib/theme-engine.js.map +1 -0
- package/dist/lib/themes.d.ts +14 -48
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +24 -164
- package/dist/lib/themes.js.map +1 -1
- package/dist/lib/tokens.d.ts +1 -25
- package/dist/lib/tokens.d.ts.map +1 -1
- package/dist/lib/tokens.js +1 -24
- package/dist/lib/tokens.js.map +1 -1
- package/dist/lib/tooltip-focus-gate.js +7 -1
- package/dist/lib/tooltip-focus-gate.js.map +1 -1
- package/dist/lib/variants.js +2 -2
- package/dist/r/lib.json +35 -0
- package/dist/r/registry.json +70 -0
- package/dist/r/src/components/theme/lazy.ts +35 -0
- package/dist/r/src/components/theme/theme-catalogue-source.ts +59 -0
- package/dist/r/src/components/theme/theme-document-core.ts +262 -0
- package/dist/r/src/components/theme/theme-document.ts +23 -240
- package/dist/r/src/components/theme/theme-persistence.ts +14 -6
- package/dist/r/src/components/theme/theme-provider-core.tsx +40 -0
- package/dist/r/src/components/theme/theme-provider-lazy.tsx +34 -0
- package/dist/r/src/components/theme/theme-provider-value.ts +445 -0
- package/dist/r/src/components/theme/theme-provider.tsx +23 -346
- package/dist/r/src/components/theme/theme-script.ts +3 -3
- package/dist/r/src/components/theme/theme-state-core.ts +364 -0
- package/dist/r/src/components/theme/theme-state.ts +10 -329
- package/dist/r/src/components/theme/theme-toggle.tsx +1 -1
- package/dist/r/src/lib/accent-presets.ts +43 -0
- package/dist/r/src/lib/theme-catalogue.ts +26 -0
- package/dist/r/src/lib/theme-css.ts +201 -0
- package/dist/r/src/lib/theme-defaults.generated.ts +319 -0
- package/dist/r/src/lib/theme-defaults.ts +79 -0
- package/dist/r/src/lib/theme-engine-light.ts +117 -0
- package/dist/r/src/lib/theme-engine.ts +111 -0
- package/dist/r/src/lib/themes.ts +27 -191
- package/dist/r/src/lib/tokens.ts +1 -35
- package/dist/r/theming.json +35 -0
- package/package.json +12 -3
- package/registry.json +70 -0
- package/src/components/theme/lazy.ts +35 -0
- package/src/components/theme/theme-catalogue-source.ts +59 -0
- package/src/components/theme/theme-document-core.ts +262 -0
- package/src/components/theme/theme-document.ts +23 -240
- package/src/components/theme/theme-persistence.ts +14 -6
- package/src/components/theme/theme-provider-core.tsx +40 -0
- package/src/components/theme/theme-provider-lazy.tsx +34 -0
- package/src/components/theme/theme-provider-value.ts +445 -0
- package/src/components/theme/theme-provider.tsx +23 -346
- package/src/components/theme/theme-script.ts +3 -3
- package/src/components/theme/theme-state-core.ts +364 -0
- package/src/components/theme/theme-state.ts +10 -329
- package/src/components/theme/theme-toggle.tsx +1 -1
- package/src/lib/accent-presets.ts +43 -0
- package/src/lib/theme-catalogue.ts +26 -0
- package/src/lib/theme-css.ts +201 -0
- package/src/lib/theme-defaults.generated.ts +319 -0
- package/src/lib/theme-defaults.ts +79 -0
- package/src/lib/theme-engine-light.ts +117 -0
- package/src/lib/theme-engine.ts +111 -0
- package/src/lib/themes.ts +27 -191
- package/src/lib/tokens.ts +1 -35
- package/dist/components/theme/theme-document.js.map +0 -1
- package/dist/components/theme/theme-state.js.map +0 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyAppearanceFontSettings,
|
|
3
|
+
normalizeAppearanceEditorFontSettings,
|
|
4
|
+
} from '#lib/appearance-font-settings'
|
|
5
|
+
import { terminalCssVariables } from '#lib/theme-css'
|
|
6
|
+
import type { ThemeEngine } from '#lib/theme-engine'
|
|
7
|
+
import type { EditorRolesMode, ThemeAppearance, ThemeVariant } from '#lib/themes'
|
|
8
|
+
import { isCustomAccent } from '#lib/accent-roles'
|
|
9
|
+
import { surfaceAlpha, type SurfaceState } from '#lib/theme-surface'
|
|
10
|
+
import { resolvedDensity, type ThemeSelection } from './theme-state-core'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The document half of the theme provider: the single writer of `<html>`.
|
|
14
|
+
*
|
|
15
|
+
* Written against the few members of an element it uses, so the rules can be tested
|
|
16
|
+
* on a stand-in and the provider's effect stays a one-line call.
|
|
17
|
+
*
|
|
18
|
+
* This is the engine-parameterised half: every question the catalogue answers goes
|
|
19
|
+
* through the {@link ThemeEngine} it is given, so it imports no catalogue code or data.
|
|
20
|
+
* `theme-document.ts` binds the full catalogue and keeps the signatures the eager
|
|
21
|
+
* provider and the tests have always used.
|
|
22
|
+
*/
|
|
23
|
+
export type ThemeRoot = Pick<
|
|
24
|
+
HTMLElement,
|
|
25
|
+
'classList' | 'dataset' | 'style' | 'removeAttribute' | 'setAttribute'
|
|
26
|
+
>
|
|
27
|
+
|
|
28
|
+
export type ThemeDocumentState = {
|
|
29
|
+
/** What the document shows: the committed selection, or a preview laid over it. */
|
|
30
|
+
selection: ThemeSelection
|
|
31
|
+
appearance: ThemeAppearance
|
|
32
|
+
variant: ThemeVariant
|
|
33
|
+
/**
|
|
34
|
+
* The variant whose terminal palette is pinned over the interface variant's.
|
|
35
|
+
* Absent, the terminal follows the interface theme.
|
|
36
|
+
*/
|
|
37
|
+
terminalVariant?: ThemeVariant
|
|
38
|
+
/**
|
|
39
|
+
* The resolved surface axis. Absent, the axis is off: no attribute and no
|
|
40
|
+
* `--surface-alpha` is written, and any this writer wrote before is removed.
|
|
41
|
+
*/
|
|
42
|
+
surface?: SurfaceState
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ThemeDocumentOptions = {
|
|
46
|
+
/**
|
|
47
|
+
* Rewrites the custom properties just before they are written: the variant's
|
|
48
|
+
* roles with the accent laid over them. It sees every property of the pass and
|
|
49
|
+
* must return the full map; a name it drops is not written.
|
|
50
|
+
*/
|
|
51
|
+
transformTokens?: (tokens: Record<string, string>) => Record<string, string>
|
|
52
|
+
/**
|
|
53
|
+
* The contrast an accent is held to against the variant's background. Absent, a
|
|
54
|
+
* preset is painted verbatim (the catalogue measured it for its own themes) and
|
|
55
|
+
* only a custom `#rrggbb` is held to the default floor of 3:1.
|
|
56
|
+
*/
|
|
57
|
+
accentContrastFloor?: number
|
|
58
|
+
/**
|
|
59
|
+
* Where a catalogue variant's palette comes from. `inline` (the default) writes
|
|
60
|
+
* every role onto the root. `stylesheet` writes none for a catalogue variant,
|
|
61
|
+
* because the host loads a stylesheet that carries a `[data-theme]` block for it;
|
|
62
|
+
* a variant the catalogue does not hold (a host's imported theme) is still written
|
|
63
|
+
* inline, since no stylesheet can know it. The accent, the terminal pin, the
|
|
64
|
+
* surface alpha and the fonts are written the same in both.
|
|
65
|
+
*
|
|
66
|
+
* Switching from an imported variant back to a catalogue one removes what the
|
|
67
|
+
* imported one wrote, so nothing inline outlives it to beat the stylesheet.
|
|
68
|
+
*/
|
|
69
|
+
palette?: 'inline' | 'stylesheet'
|
|
70
|
+
/**
|
|
71
|
+
* The `--editor-*` projection. `syntax` (the default) is the catalogue's quiet
|
|
72
|
+
* syntax roles, which is what `theme-variants.css` carries; `floor` holds every
|
|
73
|
+
* editor role to 4.5:1 on the canvas, as hex.
|
|
74
|
+
*
|
|
75
|
+
* With `palette: 'stylesheet'`, `floor` is the one palette write a catalogue
|
|
76
|
+
* variant still gets: the stylesheet is generated for the default projection, so
|
|
77
|
+
* the sixteen `--editor-*` properties are written inline over it. The rest of the
|
|
78
|
+
* palette stays in the stylesheet.
|
|
79
|
+
*/
|
|
80
|
+
editorRoles?: EditorRolesMode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What a catalogue variant still writes inline when its palette is the stylesheet's:
|
|
85
|
+
* nothing, unless the editor projection is not the one the stylesheet was generated
|
|
86
|
+
* for.
|
|
87
|
+
*/
|
|
88
|
+
function stylesheetOverrides(
|
|
89
|
+
engine: ThemeEngine,
|
|
90
|
+
variant: ThemeVariant,
|
|
91
|
+
editorRoles: EditorRolesMode | undefined
|
|
92
|
+
): Record<string, string> {
|
|
93
|
+
if (editorRoles !== 'floor') return {}
|
|
94
|
+
return Object.fromEntries(
|
|
95
|
+
Object.entries(engine.themeCssVariables(variant, { editor: 'floor' })).filter(([name]) =>
|
|
96
|
+
name.startsWith('--editor-')
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The custom properties a state paints: the variant's roles, with the accent
|
|
103
|
+
* selection laid over them, the pinned terminal palette over that, the surface
|
|
104
|
+
* alpha, then `transformTokens`.
|
|
105
|
+
*
|
|
106
|
+
* The accent is applied *here* rather than left to the `[data-accent]` blocks in
|
|
107
|
+
* `theme.css`, because the properties are written inline and an inline declaration
|
|
108
|
+
* outranks every stylesheet selector however specific it is. The blocks still exist
|
|
109
|
+
* and are still correct — they are what styles the window before this runs — but
|
|
110
|
+
* they cannot win from here, so leaving the accent to them meant the attribute was
|
|
111
|
+
* set, the rules were present, and nothing changed on screen.
|
|
112
|
+
*/
|
|
113
|
+
export function themeDocumentTokensWith(
|
|
114
|
+
engine: ThemeEngine,
|
|
115
|
+
state: ThemeDocumentState,
|
|
116
|
+
options: ThemeDocumentOptions = {}
|
|
117
|
+
): { tokens: Record<string, string>; accented: boolean } {
|
|
118
|
+
const fromStylesheet =
|
|
119
|
+
options.palette === 'stylesheet' && engine.isCatalogueVariant(state.variant)
|
|
120
|
+
const properties = fromStylesheet
|
|
121
|
+
? stylesheetOverrides(engine, state.variant, options.editorRoles)
|
|
122
|
+
: { ...engine.themeCssVariables(state.variant, { editor: options.editorRoles }) }
|
|
123
|
+
const accent = engine.accentVariables(state.selection.accent, state.appearance, state.selection, {
|
|
124
|
+
background: state.variant.colors.background,
|
|
125
|
+
floor: options.accentContrastFloor,
|
|
126
|
+
variantId: state.variant.id,
|
|
127
|
+
})
|
|
128
|
+
if (accent) Object.assign(properties, accent)
|
|
129
|
+
// Written after the variant and the accent, so it wins over the interface
|
|
130
|
+
// variant's own terminal ramp (and over the stylesheet's, which it outranks as an
|
|
131
|
+
// inline declaration). Dropping the pin needs no removal of its own: the ramp is
|
|
132
|
+
// then rewritten by the variant, or swept back to the stylesheet.
|
|
133
|
+
if (state.terminalVariant) Object.assign(properties, terminalCssVariables(state.terminalVariant))
|
|
134
|
+
if (state.surface) properties['--surface-alpha'] = surfaceAlpha[state.surface.surface]
|
|
135
|
+
return {
|
|
136
|
+
tokens: options.transformTokens ? options.transformTokens(properties) : properties,
|
|
137
|
+
accented: accent !== undefined,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Write a state onto a root and report which custom properties it owns.
|
|
143
|
+
*
|
|
144
|
+
* Pass the previous call's return as `previous` and any palette property that pass
|
|
145
|
+
* wrote and this one does not is removed. For the catalogue the two sets are the
|
|
146
|
+
* same, so nothing is ever removed; it matters for a host variant that carries
|
|
147
|
+
* `tokens` the catalogue does not, which would otherwise stay inline, beating the
|
|
148
|
+
* stylesheet, after the user switched away from it.
|
|
149
|
+
*/
|
|
150
|
+
export function applyThemeToRootWith(
|
|
151
|
+
engine: ThemeEngine,
|
|
152
|
+
root: ThemeRoot,
|
|
153
|
+
state: ThemeDocumentState,
|
|
154
|
+
options: ThemeDocumentOptions = {},
|
|
155
|
+
previous?: ReadonlySet<string>
|
|
156
|
+
): ReadonlySet<string> {
|
|
157
|
+
const { tokens, accented } = themeDocumentTokensWith(engine, state, options)
|
|
158
|
+
|
|
159
|
+
for (const [name, value] of Object.entries(tokens)) {
|
|
160
|
+
root.style.setProperty(name, value)
|
|
161
|
+
}
|
|
162
|
+
if (previous) {
|
|
163
|
+
for (const name of previous) {
|
|
164
|
+
if (!(name in tokens)) root.style.removeProperty(name)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
root.classList.toggle('dark', state.appearance === 'dark')
|
|
169
|
+
root.dataset['theme'] = state.variant.id
|
|
170
|
+
root.dataset['appearance'] = state.appearance
|
|
171
|
+
root.style.colorScheme = state.appearance
|
|
172
|
+
|
|
173
|
+
// A custom colour has no stylesheet block to select, and its value is the
|
|
174
|
+
// attribute's worst possible content, so it is reported as `custom`.
|
|
175
|
+
if (!accented) root.removeAttribute('data-accent')
|
|
176
|
+
else
|
|
177
|
+
root.dataset['accent'] = isCustomAccent(state.selection.accent)
|
|
178
|
+
? 'custom'
|
|
179
|
+
: state.selection.accent
|
|
180
|
+
|
|
181
|
+
// The surface axis is opt-in, so its absence removes rather than defaults: a host
|
|
182
|
+
// that never enables it keeps an `<html>` with neither attribute, and one that
|
|
183
|
+
// turns it off leaves nothing behind. (`--surface-alpha` is swept with the tokens.)
|
|
184
|
+
if (state.surface) {
|
|
185
|
+
root.dataset['surface'] = state.surface.surface
|
|
186
|
+
root.dataset['reduceTransparency'] = state.surface.reduceTransparencyActive ? 'true' : 'false'
|
|
187
|
+
} else {
|
|
188
|
+
root.removeAttribute('data-surface')
|
|
189
|
+
root.removeAttribute('data-reduce-transparency')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
applyAppearanceFontSettings(
|
|
193
|
+
root,
|
|
194
|
+
normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
// Density is a pure token shift, so unlike the colour axes there is nothing to
|
|
198
|
+
// compute: the attribute selects a block in `theme.css`. It is applied here for
|
|
199
|
+
// the same reason as the others — this is the one place that knows what the
|
|
200
|
+
// current selection is, and it is the only writer of these attributes.
|
|
201
|
+
if (resolvedDensity(state.selection) === 'comfortable') root.removeAttribute('data-density')
|
|
202
|
+
else root.dataset['density'] = resolvedDensity(state.selection)
|
|
203
|
+
|
|
204
|
+
return new Set(Object.keys(tokens))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** What {@link applyWithoutTransitions} needs of a document. */
|
|
208
|
+
export type TransitionDocument = {
|
|
209
|
+
head: { appendChild(node: unknown): unknown }
|
|
210
|
+
createElement(tag: 'style'): {
|
|
211
|
+
textContent: string | null
|
|
212
|
+
offsetHeight?: number
|
|
213
|
+
remove(): void
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Run `apply` with every CSS transition switched off for the frame that carries
|
|
219
|
+
* it, so a theme switch cannot animate page-wide.
|
|
220
|
+
*
|
|
221
|
+
* Without this, flipping the custom properties on `<html>` starts a transition on
|
|
222
|
+
* every element that declares one, and each fades from the old palette to the new
|
|
223
|
+
* over its own duration: a theme change that looks like a rendering fault. The
|
|
224
|
+
* rule is removed two frames later, after the browser has committed the new styles
|
|
225
|
+
* with transitions off, and `restore` runs it early (the provider calls it when it
|
|
226
|
+
* is disposed mid-switch).
|
|
227
|
+
*
|
|
228
|
+
* `schedule` is the frame source; it defaults to `requestAnimationFrame` and falls
|
|
229
|
+
* back to running at once where there is none, which leaves transitions on rather
|
|
230
|
+
* than off.
|
|
231
|
+
*/
|
|
232
|
+
export function applyWithoutTransitions(
|
|
233
|
+
doc: TransitionDocument,
|
|
234
|
+
apply: () => void,
|
|
235
|
+
schedule: (callback: () => void) => void = defaultSchedule
|
|
236
|
+
): () => void {
|
|
237
|
+
const style = doc.createElement('style')
|
|
238
|
+
style.textContent = '*{transition:none !important}'
|
|
239
|
+
doc.head.appendChild(style)
|
|
240
|
+
// Reading a layout property forces the browser to commit the rule before the
|
|
241
|
+
// properties change; without it the two can be batched into one style recalc.
|
|
242
|
+
void style.offsetHeight
|
|
243
|
+
let removed = false
|
|
244
|
+
const restore = () => {
|
|
245
|
+
if (removed) return
|
|
246
|
+
removed = true
|
|
247
|
+
style.remove()
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
apply()
|
|
251
|
+
} catch (error) {
|
|
252
|
+
restore()
|
|
253
|
+
throw error
|
|
254
|
+
}
|
|
255
|
+
schedule(() => schedule(restore))
|
|
256
|
+
return restore
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function defaultSchedule(callback: () => void): void {
|
|
260
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback)
|
|
261
|
+
else callback()
|
|
262
|
+
}
|
|
@@ -1,149 +1,36 @@
|
|
|
1
|
+
import { catalogueThemeEngine } from '#lib/theme-catalogue'
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { resolvedDensity, type ThemeSelection } from './theme-state'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The document half of the theme provider: the single writer of `<html>`.
|
|
20
|
-
*
|
|
21
|
-
* Written against the few members of an element it uses, so the rules can be tested
|
|
22
|
-
* on a stand-in and the provider's effect stays a one-line call.
|
|
23
|
-
*/
|
|
24
|
-
export type ThemeRoot = Pick<
|
|
25
|
-
HTMLElement,
|
|
26
|
-
'classList' | 'dataset' | 'style' | 'removeAttribute' | 'setAttribute'
|
|
27
|
-
>
|
|
28
|
-
|
|
29
|
-
export type ThemeDocumentState = {
|
|
30
|
-
/** What the document shows: the committed selection, or a preview laid over it. */
|
|
31
|
-
selection: ThemeSelection
|
|
32
|
-
appearance: ThemeAppearance
|
|
33
|
-
variant: ThemeVariant
|
|
34
|
-
/**
|
|
35
|
-
* The variant whose terminal palette is pinned over the interface variant's.
|
|
36
|
-
* Absent, the terminal follows the interface theme.
|
|
37
|
-
*/
|
|
38
|
-
terminalVariant?: ThemeVariant
|
|
39
|
-
/**
|
|
40
|
-
* The resolved surface axis. Absent, the axis is off: no attribute and no
|
|
41
|
-
* `--surface-alpha` is written, and any this writer wrote before is removed.
|
|
42
|
-
*/
|
|
43
|
-
surface?: SurfaceState
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export type ThemeDocumentOptions = {
|
|
47
|
-
/**
|
|
48
|
-
* Rewrites the custom properties just before they are written: the variant's
|
|
49
|
-
* roles with the accent laid over them. It sees every property of the pass and
|
|
50
|
-
* must return the full map; a name it drops is not written.
|
|
51
|
-
*/
|
|
52
|
-
transformTokens?: (tokens: Record<string, string>) => Record<string, string>
|
|
53
|
-
/**
|
|
54
|
-
* The contrast an accent is held to against the variant's background. Absent, a
|
|
55
|
-
* preset is painted verbatim (the catalogue measured it for its own themes) and
|
|
56
|
-
* only a custom `#rrggbb` is held to the default floor of 3:1.
|
|
57
|
-
*/
|
|
58
|
-
accentContrastFloor?: number
|
|
59
|
-
/**
|
|
60
|
-
* Where a catalogue variant's palette comes from. `inline` (the default) writes
|
|
61
|
-
* every role onto the root. `stylesheet` writes none for a catalogue variant,
|
|
62
|
-
* because the host loads a stylesheet that carries a `[data-theme]` block for it;
|
|
63
|
-
* a variant the catalogue does not hold (a host's imported theme) is still written
|
|
64
|
-
* inline, since no stylesheet can know it. The accent, the terminal pin, the
|
|
65
|
-
* surface alpha and the fonts are written the same in both.
|
|
66
|
-
*
|
|
67
|
-
* Switching from an imported variant back to a catalogue one removes what the
|
|
68
|
-
* imported one wrote, so nothing inline outlives it to beat the stylesheet.
|
|
69
|
-
*/
|
|
70
|
-
palette?: 'inline' | 'stylesheet'
|
|
71
|
-
/**
|
|
72
|
-
* The `--editor-*` projection. `syntax` (the default) is the catalogue's quiet
|
|
73
|
-
* syntax roles, which is what `theme-variants.css` carries; `floor` holds every
|
|
74
|
-
* editor role to 4.5:1 on the canvas, as hex.
|
|
75
|
-
*
|
|
76
|
-
* With `palette: 'stylesheet'`, `floor` is the one palette write a catalogue
|
|
77
|
-
* variant still gets: the stylesheet is generated for the default projection, so
|
|
78
|
-
* the sixteen `--editor-*` properties are written inline over it. The rest of the
|
|
79
|
-
* palette stays in the stylesheet.
|
|
80
|
-
*/
|
|
81
|
-
editorRoles?: EditorRolesMode
|
|
82
|
-
}
|
|
3
|
+
applyThemeToRootWith,
|
|
4
|
+
themeDocumentTokensWith,
|
|
5
|
+
type ThemeDocumentOptions,
|
|
6
|
+
type ThemeDocumentState,
|
|
7
|
+
type ThemeRoot,
|
|
8
|
+
} from './theme-document-core'
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
applyWithoutTransitions,
|
|
12
|
+
type ThemeDocumentOptions,
|
|
13
|
+
type ThemeDocumentState,
|
|
14
|
+
type ThemeRoot,
|
|
15
|
+
type TransitionDocument,
|
|
16
|
+
} from './theme-document-core'
|
|
83
17
|
|
|
84
18
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
function stylesheetOverrides(
|
|
90
|
-
variant: ThemeVariant,
|
|
91
|
-
editorRoles: EditorRolesMode | undefined
|
|
92
|
-
): Record<string, string> {
|
|
93
|
-
if (editorRoles !== 'floor') return {}
|
|
94
|
-
return Object.fromEntries(
|
|
95
|
-
Object.entries(themeCssVariables(variant, { editor: 'floor' })).filter(([name]) =>
|
|
96
|
-
name.startsWith('--editor-')
|
|
97
|
-
)
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The custom properties a state paints: the variant's roles, with the accent
|
|
103
|
-
* selection laid over them, the pinned terminal palette over that, the surface
|
|
104
|
-
* alpha, then `transformTokens`.
|
|
105
|
-
*
|
|
106
|
-
* The accent is applied *here* rather than left to the `[data-accent]` blocks in
|
|
107
|
-
* `theme.css`, because the properties are written inline and an inline declaration
|
|
108
|
-
* outranks every stylesheet selector however specific it is. The blocks still exist
|
|
109
|
-
* and are still correct — they are what styles the window before this runs — but
|
|
110
|
-
* they cannot win from here, so leaving the accent to them meant the attribute was
|
|
111
|
-
* set, the rules were present, and nothing changed on screen.
|
|
19
|
+
* The custom properties a state paints, resolved against the whole catalogue: the
|
|
20
|
+
* variant's roles, with the accent selection laid over them, the pinned terminal
|
|
21
|
+
* palette over that, the surface alpha, then `transformTokens`. See
|
|
22
|
+
* {@link themeDocumentTokensWith} for the rules.
|
|
112
23
|
*/
|
|
113
24
|
export function themeDocumentTokens(
|
|
114
25
|
state: ThemeDocumentState,
|
|
115
26
|
options: ThemeDocumentOptions = {}
|
|
116
27
|
): { tokens: Record<string, string>; accented: boolean } {
|
|
117
|
-
|
|
118
|
-
const properties = fromStylesheet
|
|
119
|
-
? stylesheetOverrides(state.variant, options.editorRoles)
|
|
120
|
-
: { ...themeCssVariables(state.variant, { editor: options.editorRoles }) }
|
|
121
|
-
const accent = accentVariables(state.selection.accent, state.appearance, state.selection, {
|
|
122
|
-
background: state.variant.colors.background,
|
|
123
|
-
floor: options.accentContrastFloor,
|
|
124
|
-
variantId: state.variant.id,
|
|
125
|
-
})
|
|
126
|
-
if (accent) Object.assign(properties, accent)
|
|
127
|
-
// Written after the variant and the accent, so it wins over the interface
|
|
128
|
-
// variant's own terminal ramp (and over the stylesheet's, which it outranks as an
|
|
129
|
-
// inline declaration). Dropping the pin needs no removal of its own: the ramp is
|
|
130
|
-
// then rewritten by the variant, or swept back to the stylesheet.
|
|
131
|
-
if (state.terminalVariant) Object.assign(properties, terminalCssVariables(state.terminalVariant))
|
|
132
|
-
if (state.surface) properties['--surface-alpha'] = surfaceAlpha[state.surface.surface]
|
|
133
|
-
return {
|
|
134
|
-
tokens: options.transformTokens ? options.transformTokens(properties) : properties,
|
|
135
|
-
accented: accent !== undefined,
|
|
136
|
-
}
|
|
28
|
+
return themeDocumentTokensWith(catalogueThemeEngine, state, options)
|
|
137
29
|
}
|
|
138
30
|
|
|
139
31
|
/**
|
|
140
|
-
* Write a state onto a root and report which custom properties it owns
|
|
141
|
-
*
|
|
142
|
-
* Pass the previous call's return as `previous` and any palette property that pass
|
|
143
|
-
* wrote and this one does not is removed. For the catalogue the two sets are the
|
|
144
|
-
* same, so nothing is ever removed; it matters for a host variant that carries
|
|
145
|
-
* `tokens` the catalogue does not, which would otherwise stay inline, beating the
|
|
146
|
-
* stylesheet, after the user switched away from it.
|
|
32
|
+
* Write a state onto a root and report which custom properties it owns, resolved
|
|
33
|
+
* against the whole catalogue. See {@link applyThemeToRootWith} for the rules.
|
|
147
34
|
*/
|
|
148
35
|
export function applyThemeToRoot(
|
|
149
36
|
root: ThemeRoot,
|
|
@@ -151,109 +38,5 @@ export function applyThemeToRoot(
|
|
|
151
38
|
options: ThemeDocumentOptions = {},
|
|
152
39
|
previous?: ReadonlySet<string>
|
|
153
40
|
): ReadonlySet<string> {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const [name, value] of Object.entries(tokens)) {
|
|
157
|
-
root.style.setProperty(name, value)
|
|
158
|
-
}
|
|
159
|
-
if (previous) {
|
|
160
|
-
for (const name of previous) {
|
|
161
|
-
if (!(name in tokens)) root.style.removeProperty(name)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
root.classList.toggle('dark', state.appearance === 'dark')
|
|
166
|
-
root.dataset['theme'] = state.variant.id
|
|
167
|
-
root.dataset['appearance'] = state.appearance
|
|
168
|
-
root.style.colorScheme = state.appearance
|
|
169
|
-
|
|
170
|
-
// A custom colour has no stylesheet block to select, and its value is the
|
|
171
|
-
// attribute's worst possible content, so it is reported as `custom`.
|
|
172
|
-
if (!accented) root.removeAttribute('data-accent')
|
|
173
|
-
else
|
|
174
|
-
root.dataset['accent'] = isCustomAccent(state.selection.accent)
|
|
175
|
-
? 'custom'
|
|
176
|
-
: state.selection.accent
|
|
177
|
-
|
|
178
|
-
// The surface axis is opt-in, so its absence removes rather than defaults: a host
|
|
179
|
-
// that never enables it keeps an `<html>` with neither attribute, and one that
|
|
180
|
-
// turns it off leaves nothing behind. (`--surface-alpha` is swept with the tokens.)
|
|
181
|
-
if (state.surface) {
|
|
182
|
-
root.dataset['surface'] = state.surface.surface
|
|
183
|
-
root.dataset['reduceTransparency'] = state.surface.reduceTransparencyActive ? 'true' : 'false'
|
|
184
|
-
} else {
|
|
185
|
-
root.removeAttribute('data-surface')
|
|
186
|
-
root.removeAttribute('data-reduce-transparency')
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
applyAppearanceFontSettings(
|
|
190
|
-
root,
|
|
191
|
-
normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
// Density is a pure token shift, so unlike the colour axes there is nothing to
|
|
195
|
-
// compute: the attribute selects a block in `theme.css`. It is applied here for
|
|
196
|
-
// the same reason as the others — this is the one place that knows what the
|
|
197
|
-
// current selection is, and it is the only writer of these attributes.
|
|
198
|
-
if (resolvedDensity(state.selection) === 'comfortable') root.removeAttribute('data-density')
|
|
199
|
-
else root.dataset['density'] = resolvedDensity(state.selection)
|
|
200
|
-
|
|
201
|
-
return new Set(Object.keys(tokens))
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** What {@link applyWithoutTransitions} needs of a document. */
|
|
205
|
-
export type TransitionDocument = {
|
|
206
|
-
head: { appendChild(node: unknown): unknown }
|
|
207
|
-
createElement(tag: 'style'): {
|
|
208
|
-
textContent: string | null
|
|
209
|
-
offsetHeight?: number
|
|
210
|
-
remove(): void
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Run `apply` with every CSS transition switched off for the frame that carries
|
|
216
|
-
* it, so a theme switch cannot animate page-wide.
|
|
217
|
-
*
|
|
218
|
-
* Without this, flipping the custom properties on `<html>` starts a transition on
|
|
219
|
-
* every element that declares one, and each fades from the old palette to the new
|
|
220
|
-
* over its own duration: a theme change that looks like a rendering fault. The
|
|
221
|
-
* rule is removed two frames later, after the browser has committed the new styles
|
|
222
|
-
* with transitions off, and `restore` runs it early (the provider calls it when it
|
|
223
|
-
* is disposed mid-switch).
|
|
224
|
-
*
|
|
225
|
-
* `schedule` is the frame source; it defaults to `requestAnimationFrame` and falls
|
|
226
|
-
* back to running at once where there is none, which leaves transitions on rather
|
|
227
|
-
* than off.
|
|
228
|
-
*/
|
|
229
|
-
export function applyWithoutTransitions(
|
|
230
|
-
doc: TransitionDocument,
|
|
231
|
-
apply: () => void,
|
|
232
|
-
schedule: (callback: () => void) => void = defaultSchedule
|
|
233
|
-
): () => void {
|
|
234
|
-
const style = doc.createElement('style')
|
|
235
|
-
style.textContent = '*{transition:none !important}'
|
|
236
|
-
doc.head.appendChild(style)
|
|
237
|
-
// Reading a layout property forces the browser to commit the rule before the
|
|
238
|
-
// properties change; without it the two can be batched into one style recalc.
|
|
239
|
-
void style.offsetHeight
|
|
240
|
-
let removed = false
|
|
241
|
-
const restore = () => {
|
|
242
|
-
if (removed) return
|
|
243
|
-
removed = true
|
|
244
|
-
style.remove()
|
|
245
|
-
}
|
|
246
|
-
try {
|
|
247
|
-
apply()
|
|
248
|
-
} catch (error) {
|
|
249
|
-
restore()
|
|
250
|
-
throw error
|
|
251
|
-
}
|
|
252
|
-
schedule(() => schedule(restore))
|
|
253
|
-
return restore
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function defaultSchedule(callback: () => void): void {
|
|
257
|
-
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback)
|
|
258
|
-
else callback()
|
|
41
|
+
return applyThemeToRootWith(catalogueThemeEngine, root, state, options, previous)
|
|
259
42
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { normalizeCustomAccent } from '#lib/accent-roles'
|
|
2
|
-
import { accentPresets } from '#lib/
|
|
2
|
+
import { accentPresets } from '#lib/accent-presets'
|
|
3
3
|
import { normalizeAppearanceEditorFontSettings } from '#lib/appearance-font-settings'
|
|
4
|
-
import {
|
|
4
|
+
import { isThemeAccentId } from '#lib/theme-accents'
|
|
5
|
+
import {
|
|
6
|
+
catalogueThemeAppearance,
|
|
7
|
+
defaultDarkThemeId,
|
|
8
|
+
defaultLightThemeId,
|
|
9
|
+
} from '#lib/theme-defaults'
|
|
5
10
|
import { normalizeSurface } from '#lib/theme-surface'
|
|
6
11
|
import {
|
|
7
12
|
defaultPreferenceStorage,
|
|
@@ -11,7 +16,7 @@ import {
|
|
|
11
16
|
writeVersionedRecord,
|
|
12
17
|
type PreferenceStorage,
|
|
13
18
|
} from '#lib/preference-store'
|
|
14
|
-
import { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state'
|
|
19
|
+
import { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state-core'
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Where a selection is kept between sessions.
|
|
@@ -51,7 +56,10 @@ function preferenceStore(): Storage | undefined {
|
|
|
51
56
|
return typeof localStorage === 'undefined' ? undefined : localStorage
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* The stored preference, validated against the catalogue's ids (`theme-defaults`), so
|
|
61
|
+
* reading it never loads the catalogue.
|
|
62
|
+
*/
|
|
55
63
|
function readSelection(storageKey: string): ThemeSelection {
|
|
56
64
|
try {
|
|
57
65
|
const raw = preferenceStore()?.getItem(storageKey)
|
|
@@ -68,11 +76,11 @@ function readSelection(storageKey: string): ThemeSelection {
|
|
|
68
76
|
// An unknown id would leave the document with no theme at all, so a stale
|
|
69
77
|
// preference from an older catalogue falls back rather than applying nothing.
|
|
70
78
|
lightThemeId:
|
|
71
|
-
|
|
79
|
+
catalogueThemeAppearance(parsed.lightThemeId ?? '') === 'light'
|
|
72
80
|
? (parsed.lightThemeId as string)
|
|
73
81
|
: defaultLightThemeId,
|
|
74
82
|
darkThemeId:
|
|
75
|
-
|
|
83
|
+
catalogueThemeAppearance(parsed.darkThemeId ?? '') === 'dark'
|
|
76
84
|
? (parsed.darkThemeId as string)
|
|
77
85
|
: defaultDarkThemeId,
|
|
78
86
|
// A theme accent is kept whenever it names a slot, even one the stored pair
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { themeContext } from './theme-context'
|
|
2
|
+
import {
|
|
3
|
+
createThemeContextValue,
|
|
4
|
+
type ThemeCatalogueSource,
|
|
5
|
+
type ThemeProviderProps,
|
|
6
|
+
} from './theme-provider-value'
|
|
7
|
+
|
|
8
|
+
export type { ThemeCatalogueSource, ThemeProviderProps } from './theme-provider-value'
|
|
9
|
+
export {
|
|
10
|
+
createVersionedPersistence,
|
|
11
|
+
localStoragePersistence,
|
|
12
|
+
type ThemePersistence,
|
|
13
|
+
type ThemeStorageSchema,
|
|
14
|
+
type VersionedCodec,
|
|
15
|
+
type VersionedPersistenceOptions,
|
|
16
|
+
} from './theme-persistence'
|
|
17
|
+
export { useOptionalTheme, useTheme } from './theme-context'
|
|
18
|
+
export type { ThemeSurface } from '#lib/theme-surface'
|
|
19
|
+
export type { EditorRolesMode } from '#lib/themes'
|
|
20
|
+
export { inlineScriptLiteral, themeScript, type ThemeScriptOptions } from './theme-script'
|
|
21
|
+
export {
|
|
22
|
+
defaultThemeSelection,
|
|
23
|
+
resolvedDensity,
|
|
24
|
+
type ThemeContextValue,
|
|
25
|
+
type ThemeDensity,
|
|
26
|
+
type ThemeSelection,
|
|
27
|
+
} from './theme-state-core'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The provider, parameterised by where its catalogue comes from.
|
|
31
|
+
*
|
|
32
|
+
* `ThemeProvider` (`theme-provider.tsx`) and the lazy `ThemeProvider`
|
|
33
|
+
* (`theme-provider-lazy.tsx`) are this component with a source; the work is
|
|
34
|
+
* `createThemeContextValue`. It imports no catalogue code or data.
|
|
35
|
+
*/
|
|
36
|
+
export function ThemeProviderCore(props: ThemeProviderProps & { catalogue: ThemeCatalogueSource }) {
|
|
37
|
+
const value = createThemeContextValue(props)
|
|
38
|
+
const Context = themeContext()
|
|
39
|
+
return <Context.Provider value={value}>{props.children}</Context.Provider>
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createLazyCatalogue } from './theme-catalogue-source'
|
|
2
|
+
import { ThemeProviderCore, type ThemeProviderProps } from './theme-provider-core'
|
|
3
|
+
|
|
4
|
+
export { loadThemeCatalogue } from './theme-catalogue-source'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ThemeProvider, with the theme catalogue out of the start-up path.
|
|
8
|
+
*
|
|
9
|
+
* Same props, same context and same behaviour as `ThemeProvider` for everything the
|
|
10
|
+
* shipped default pair covers: the default light and dark themes, a host's own
|
|
11
|
+
* `themes`, every accent preset and custom colour, the surface axis, fonts and
|
|
12
|
+
* density. Only the 32 other catalogue themes are not in the start-up chunk. The first
|
|
13
|
+
* time the selection (or a preview) names one, or a theme-derived `ansi-*` accent over
|
|
14
|
+
* such a pair, this provider imports the catalogue in a chunk of its own:
|
|
15
|
+
*
|
|
16
|
+
* - until it arrives the document keeps what is on it (set before first paint by
|
|
17
|
+
* `themeScript`, painted by the stylesheet under `palette="stylesheet"`), and
|
|
18
|
+
* `variant()` is the default of the appearance;
|
|
19
|
+
* - when it arrives the provider re-resolves and paints the stored theme, and
|
|
20
|
+
* `useTheme().catalogueReady()` turns true;
|
|
21
|
+
* - a preview of a theme that has not loaded starts the load and shows when it ends.
|
|
22
|
+
*
|
|
23
|
+
* Pair it with `palette="stylesheet"` and `themeScript`: inline palettes of catalogue
|
|
24
|
+
* themes need the catalogue to compute, so under the default `inline` palette every
|
|
25
|
+
* stored theme but the defaults waits for the import.
|
|
26
|
+
*
|
|
27
|
+
* `useTheme().themes` and `allThemes()` list only the default pair until
|
|
28
|
+
* `catalogueReady()`; a picker calls `useTheme().loadCatalogue()` when it mounts.
|
|
29
|
+
*/
|
|
30
|
+
export function ThemeProvider(props: ThemeProviderProps) {
|
|
31
|
+
// Hoisted: a JSX prop expression is re-evaluated on every read, and this source owns signals.
|
|
32
|
+
const catalogue = createLazyCatalogue()
|
|
33
|
+
return <ThemeProviderCore {...props} catalogue={catalogue} />
|
|
34
|
+
}
|