@ccheever/exact-facet-core 0.1.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/facet-registry.json +548 -0
- package/package.json +22 -0
- package/src/__tests__/facet-overrides.test.ts +91 -0
- package/src/__tests__/theme-definition-resolution.test.ts +235 -0
- package/src/__tests__/theme-store-parity.test.ts +283 -0
- package/src/facet-catalog.ts +75 -0
- package/src/facet-overrides.ts +322 -0
- package/src/facet-params.generated.ts +65 -0
- package/src/facet-scorecard.ts +60 -0
- package/src/index.ts +16 -0
- package/src/internals.ts +1313 -0
- package/src/provenance-trace.ts +153 -0
- package/src/store-machinery.ts +128 -0
- package/src/theme-definition.ts +566 -0
- package/src/theme-store.ts +456 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// @system @ref LLP 0157 §2 — the consolidated Facet theme store.
|
|
2
|
+
//
|
|
3
|
+
// One store, many bindings: the React `FacetThemeProvider` registers a scope
|
|
4
|
+
// per provider instance (preserving today's subtree semantics), a Contract
|
|
5
|
+
// root registers a scope through the theme capability, and the agent
|
|
6
|
+
// endpoints read/write the same process-global inputs. The semantics here
|
|
7
|
+
// are a written contract (LLP 0157 §2) with parity tests — resolution must
|
|
8
|
+
// match the legacy provider exactly:
|
|
9
|
+
//
|
|
10
|
+
// precedence (highest wins): agent override → scope `overrides`
|
|
11
|
+
// → scope custom theme → base scheme
|
|
12
|
+
// animation passes through applyMotionPreference(reduced motion) at
|
|
13
|
+
// resolution time
|
|
14
|
+
// the designated default scope publishes the agent snapshot in the
|
|
15
|
+
// legacy shape `{ scheme, themeName, tokens{7}, updatedAt }`
|
|
16
|
+
// contrast validation warns when any override is active
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
getThemeOverride,
|
|
20
|
+
setThemeSnapshot,
|
|
21
|
+
subscribeThemeOverride,
|
|
22
|
+
} from '@exact/core/agent/experience-registry';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
applyMotionPreference,
|
|
26
|
+
deepMerge,
|
|
27
|
+
resolveSizeClassForWidth,
|
|
28
|
+
validateFacetContrast,
|
|
29
|
+
warnFacetOnce,
|
|
30
|
+
type DeepPartial,
|
|
31
|
+
type FacetTheme,
|
|
32
|
+
type Scheme,
|
|
33
|
+
} from './internals.js';
|
|
34
|
+
import {
|
|
35
|
+
defaultThemeDefinition,
|
|
36
|
+
resolveFacetTheme,
|
|
37
|
+
type FacetThemeDefinition,
|
|
38
|
+
} from './theme-definition.js';
|
|
39
|
+
import {
|
|
40
|
+
createHostInput,
|
|
41
|
+
createPreferenceStore,
|
|
42
|
+
type HostInput,
|
|
43
|
+
type PreferenceStore,
|
|
44
|
+
} from './store-machinery.js';
|
|
45
|
+
import {
|
|
46
|
+
getAppThemeOverrides,
|
|
47
|
+
getThemeDefinitionOverrides,
|
|
48
|
+
subscribeFacetOverrides,
|
|
49
|
+
} from './facet-overrides.js';
|
|
50
|
+
|
|
51
|
+
export type ThemeSchemePreference = Scheme | 'system';
|
|
52
|
+
|
|
53
|
+
export interface ThemeScopeConfig {
|
|
54
|
+
/** Mirrors `FacetThemeProvider`'s `theme` prop: a scheme, 'system', or a full custom theme. */
|
|
55
|
+
theme?: ThemeSchemePreference | FacetTheme;
|
|
56
|
+
/** Mirrors `FacetThemeProvider`'s `scheme` prop; used when `theme` is absent. */
|
|
57
|
+
scheme?: ThemeSchemePreference;
|
|
58
|
+
/** Deep-partial token overrides, merged above the base theme. */
|
|
59
|
+
overrides?: DeepPartial<FacetTheme> | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ThemeScope {
|
|
63
|
+
readonly id: number;
|
|
64
|
+
/** Resolved theme for this scope (cached until an input or config changes). */
|
|
65
|
+
getTheme(): FacetTheme;
|
|
66
|
+
subscribe(listener: () => void): () => void;
|
|
67
|
+
update(config: ThemeScopeConfig): void;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
/** Whether this scope currently backs the agent snapshot. */
|
|
70
|
+
isDefault(): boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ScopeRecord {
|
|
74
|
+
id: number;
|
|
75
|
+
config: ThemeScopeConfig;
|
|
76
|
+
listeners: Set<() => void>;
|
|
77
|
+
cachedTheme: FacetTheme | null;
|
|
78
|
+
cachedAtVersion: number;
|
|
79
|
+
disposed: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Process-global inputs (LLP 0157 §2 "Inputs")
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/** Host-fed system appearance. Bindings feed this from their host signal. */
|
|
87
|
+
const systemAppearanceInput: HostInput<Scheme> = createHostInput<Scheme>('light');
|
|
88
|
+
|
|
89
|
+
/** Host-fed reduced-motion preference. */
|
|
90
|
+
const reducedMotionInput: HostInput<boolean> = createHostInput<boolean>(false);
|
|
91
|
+
|
|
92
|
+
/** Host-fed viewport width for LLP 0195 size-class resolution. */
|
|
93
|
+
const viewportWidthInput: HostInput<number> = createHostInput<number>(390);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Process-global scheme preference with injected persistence (used by scopes
|
|
97
|
+
* that do not pin a theme/scheme locally; the React provider always passes
|
|
98
|
+
* its props, so this input only matters for preference-driven bindings).
|
|
99
|
+
*/
|
|
100
|
+
const schemePreferenceStore: PreferenceStore<ThemeSchemePreference> = createPreferenceStore({
|
|
101
|
+
defaultValue: 'system',
|
|
102
|
+
normalize: (raw) => (raw === 'light' || raw === 'dark' || raw === 'system' ? raw : null),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let globalVersion = 0;
|
|
106
|
+
const scopes = new Map<number, ScopeRecord>();
|
|
107
|
+
const scopeOrder: number[] = [];
|
|
108
|
+
const storeListeners = new Set<() => void>();
|
|
109
|
+
let nextScopeId = 1;
|
|
110
|
+
let agentOverrideSubscription: (() => void) | null = null;
|
|
111
|
+
|
|
112
|
+
function bumpGlobalVersion(): void {
|
|
113
|
+
globalVersion += 1;
|
|
114
|
+
for (const id of scopeOrder) {
|
|
115
|
+
const record = scopes.get(id);
|
|
116
|
+
if (!record) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const listener of record.listeners) {
|
|
120
|
+
listener();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const listener of storeListeners) {
|
|
124
|
+
listener();
|
|
125
|
+
}
|
|
126
|
+
publishDefaultScopeSnapshot();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
systemAppearanceInput.subscribe(bumpGlobalVersion);
|
|
130
|
+
reducedMotionInput.subscribe(bumpGlobalVersion);
|
|
131
|
+
viewportWidthInput.subscribe(bumpGlobalVersion);
|
|
132
|
+
schemePreferenceStore.subscribe(bumpGlobalVersion);
|
|
133
|
+
|
|
134
|
+
function ensureAgentOverrideSubscription(): void {
|
|
135
|
+
// Design Mode's file-backed overrides (LLP 0158 §6.2) are an input like
|
|
136
|
+
// the agent registry: a change to either layer bumps the global version.
|
|
137
|
+
ensureFacetOverridesSubscription();
|
|
138
|
+
if (agentOverrideSubscription) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// The pre-F0 standalone override registry in @exact/core folds into the
|
|
142
|
+
// store as an input: POST /agent/theme/override still writes the registry,
|
|
143
|
+
// and the store reacts to it like any other input.
|
|
144
|
+
agentOverrideSubscription = subscribeThemeOverride(bumpGlobalVersion);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let facetOverridesSubscription: (() => void) | null = null;
|
|
148
|
+
|
|
149
|
+
function ensureFacetOverridesSubscription(): void {
|
|
150
|
+
if (facetOverridesSubscription) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
facetOverridesSubscription = subscribeFacetOverrides(bumpGlobalVersion);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function setThemeSystemAppearance(scheme: Scheme): void {
|
|
157
|
+
systemAppearanceInput.set(scheme);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getThemeSystemAppearance(): Scheme {
|
|
161
|
+
return systemAppearanceInput.get();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function setThemeReducedMotion(reducedMotion: boolean): void {
|
|
165
|
+
reducedMotionInput.set(reducedMotion);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function getThemeReducedMotion(): boolean {
|
|
169
|
+
return reducedMotionInput.get();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function setThemeViewportWidth(width: number): void {
|
|
173
|
+
viewportWidthInput.set(Number.isFinite(width) ? Math.max(0, width) : 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getThemeViewportWidth(): number {
|
|
177
|
+
return viewportWidthInput.get();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function setThemeSchemePreference(preference: ThemeSchemePreference): void {
|
|
181
|
+
schemePreferenceStore.set(preference);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getThemeSchemePreference(): ThemeSchemePreference {
|
|
185
|
+
return schemePreferenceStore.get();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const configureThemePreferencePersistence =
|
|
189
|
+
schemePreferenceStore.configurePersistence.bind(schemePreferenceStore);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Subscribes to any process-global theme input change (system appearance,
|
|
193
|
+
* reduced motion, scheme preference, agent override). Bindings use this with
|
|
194
|
+
* `getThemeStoreVersion` for useSyncExternalStore-style integration.
|
|
195
|
+
*/
|
|
196
|
+
export function subscribeToThemeStore(listener: () => void): () => void {
|
|
197
|
+
ensureAgentOverrideSubscription();
|
|
198
|
+
storeListeners.add(listener);
|
|
199
|
+
return () => {
|
|
200
|
+
storeListeners.delete(listener);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getThemeStoreVersion(): number {
|
|
205
|
+
return globalVersion;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Resolution (verbatim semantics from the legacy FacetThemeProvider)
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
export interface ThemeResolutionInputs {
|
|
213
|
+
/** Overrides the host-fed system appearance for this resolution. */
|
|
214
|
+
systemAppearance?: Scheme;
|
|
215
|
+
/** Overrides the host-fed reduced-motion preference for this resolution. */
|
|
216
|
+
reducedMotion?: boolean;
|
|
217
|
+
/** Overrides the host-fed viewport width for size-class token resolution. */
|
|
218
|
+
viewportWidth?: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveScopeTheme(config: ThemeScopeConfig, inputs?: ThemeResolutionInputs): {
|
|
222
|
+
theme: FacetTheme;
|
|
223
|
+
hasCustomThemeOverride: boolean;
|
|
224
|
+
} {
|
|
225
|
+
const registryOverride = getThemeOverride();
|
|
226
|
+
const requestedTheme = config.theme ?? config.scheme ?? schemePreferenceStore.get();
|
|
227
|
+
const systemAppearance = inputs?.systemAppearance ?? systemAppearanceInput.get();
|
|
228
|
+
const prefersReducedMotion = inputs?.reducedMotion ?? reducedMotionInput.get();
|
|
229
|
+
const viewportWidth = inputs?.viewportWidth ?? viewportWidthInput.get();
|
|
230
|
+
|
|
231
|
+
// LLP 0269 cascade, lowest → highest: definition → resolved tokens (level 1)
|
|
232
|
+
// < app token overrides (theme.overrides.ts, level 3) < scope/instance
|
|
233
|
+
// overrides < agent registry override. A dial edit is broad; a token/instance
|
|
234
|
+
// override wins locally ("most-specific wins").
|
|
235
|
+
const definitionOverrides = getThemeDefinitionOverrides();
|
|
236
|
+
const activeDefinition: FacetThemeDefinition = definitionOverrides
|
|
237
|
+
? deepMerge(defaultThemeDefinition, definitionOverrides)
|
|
238
|
+
: defaultThemeDefinition;
|
|
239
|
+
|
|
240
|
+
const baseTheme: FacetTheme =
|
|
241
|
+
typeof requestedTheme === 'object'
|
|
242
|
+
// A fully-resolved custom theme passed directly (rare; the React provider
|
|
243
|
+
// still allows it). Used verbatim as the cascade base.
|
|
244
|
+
? requestedTheme
|
|
245
|
+
: resolveFacetTheme(activeDefinition, {
|
|
246
|
+
scheme:
|
|
247
|
+
(requestedTheme === 'system' ? systemAppearance : requestedTheme) === 'dark'
|
|
248
|
+
? 'dark'
|
|
249
|
+
: 'light',
|
|
250
|
+
reducedMotion: prefersReducedMotion,
|
|
251
|
+
viewportWidth,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const appOverrides = getAppThemeOverrides();
|
|
255
|
+
const mergedTheme = deepMerge(
|
|
256
|
+
deepMerge(deepMerge(baseTheme, appOverrides), config.overrides),
|
|
257
|
+
registryOverride as DeepPartial<FacetTheme> | null | undefined,
|
|
258
|
+
);
|
|
259
|
+
const hasCustomThemeOverride =
|
|
260
|
+
typeof requestedTheme === 'object' ||
|
|
261
|
+
config.overrides != null ||
|
|
262
|
+
appOverrides != null ||
|
|
263
|
+
definitionOverrides != null ||
|
|
264
|
+
registryOverride != null;
|
|
265
|
+
const theme: FacetTheme = {
|
|
266
|
+
...mergedTheme,
|
|
267
|
+
sizeClass: resolveSizeClassForWidth(viewportWidth, mergedTheme.sizeClasses),
|
|
268
|
+
motion: applyMotionPreference(mergedTheme.motion, prefersReducedMotion),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return { theme, hasCustomThemeOverride };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function validateScopeContrast(theme: FacetTheme, hasCustomThemeOverride: boolean): void {
|
|
275
|
+
if (!hasCustomThemeOverride) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const failures = validateFacetContrast(theme);
|
|
279
|
+
if (failures.length === 0) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
warnFacetOnce(`[Facet] Theme override contrast check failed: ${failures.join('; ')}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Snapshot publication (default scope only)
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
function defaultScopeRecord(): ScopeRecord | null {
|
|
290
|
+
// Newest live scope backs the agent snapshot. This matches the legacy
|
|
291
|
+
// multi-provider outcome at mount (every provider's effect republished
|
|
292
|
+
// last-write-wins, and React runs parent effects after children, so the
|
|
293
|
+
// outermost provider won — and it registers last here too).
|
|
294
|
+
for (let index = scopeOrder.length - 1; index >= 0; index -= 1) {
|
|
295
|
+
const record = scopes.get(scopeOrder[index]);
|
|
296
|
+
if (record && !record.disposed) {
|
|
297
|
+
return record;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function publishDefaultScopeSnapshot(): void {
|
|
304
|
+
const record = defaultScopeRecord();
|
|
305
|
+
if (!record) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const { theme, hasCustomThemeOverride } = resolveScopeTheme(record.config);
|
|
309
|
+
validateScopeContrast(theme, hasCustomThemeOverride);
|
|
310
|
+
setThemeSnapshot({
|
|
311
|
+
scheme: theme.scheme,
|
|
312
|
+
themeName: theme.name,
|
|
313
|
+
// The agent snapshot keeps its stable 7-token shape, now sourced from the
|
|
314
|
+
// resolved semantic taxonomy (LLP 0269): old surface→card (raised),
|
|
315
|
+
// old surfaceAlt→surface (recessed).
|
|
316
|
+
tokens: {
|
|
317
|
+
background: theme.color.background.body,
|
|
318
|
+
surface: theme.color.background.card,
|
|
319
|
+
surfaceAlt: theme.color.background.surface,
|
|
320
|
+
border: theme.color.border.default,
|
|
321
|
+
text: theme.color.text.primary,
|
|
322
|
+
accent: theme.color.accent.fill,
|
|
323
|
+
muted: theme.color.text.secondary,
|
|
324
|
+
},
|
|
325
|
+
updatedAt: Date.now(),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Scopes (LLP 0157 §2 "Scopes")
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Registers a theme scope. The first live scope is the designated default
|
|
335
|
+
* scope and backs the agent snapshot; when it disposes, the next-oldest
|
|
336
|
+
* scope takes over. (Pre-F0, multiple React providers raced on the snapshot
|
|
337
|
+
* effect last-write-wins; first-registered-wins is the strictly-better
|
|
338
|
+
* clarification recorded in LLP 0157 §2.)
|
|
339
|
+
*/
|
|
340
|
+
export function registerThemeScope(config: ThemeScopeConfig = {}): ThemeScope {
|
|
341
|
+
ensureAgentOverrideSubscription();
|
|
342
|
+
|
|
343
|
+
const record: ScopeRecord = {
|
|
344
|
+
id: nextScopeId++,
|
|
345
|
+
config,
|
|
346
|
+
listeners: new Set(),
|
|
347
|
+
cachedTheme: null,
|
|
348
|
+
cachedAtVersion: -1,
|
|
349
|
+
disposed: false,
|
|
350
|
+
};
|
|
351
|
+
scopes.set(record.id, record);
|
|
352
|
+
scopeOrder.push(record.id);
|
|
353
|
+
publishDefaultScopeSnapshot();
|
|
354
|
+
|
|
355
|
+
let scopeVersion = 0;
|
|
356
|
+
|
|
357
|
+
function resolutionVersion(): number {
|
|
358
|
+
// Cache key over every input that can change resolution output.
|
|
359
|
+
return globalVersion * 1_000_000 + scopeVersion;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
id: record.id,
|
|
364
|
+
|
|
365
|
+
getTheme(): FacetTheme {
|
|
366
|
+
const version = resolutionVersion();
|
|
367
|
+
if (record.cachedTheme && record.cachedAtVersion === version) {
|
|
368
|
+
return record.cachedTheme;
|
|
369
|
+
}
|
|
370
|
+
const { theme, hasCustomThemeOverride } = resolveScopeTheme(record.config);
|
|
371
|
+
validateScopeContrast(theme, hasCustomThemeOverride);
|
|
372
|
+
record.cachedTheme = theme;
|
|
373
|
+
record.cachedAtVersion = version;
|
|
374
|
+
return theme;
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
subscribe(listener: () => void): () => void {
|
|
378
|
+
record.listeners.add(listener);
|
|
379
|
+
return () => {
|
|
380
|
+
record.listeners.delete(listener);
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
update(config: ThemeScopeConfig): void {
|
|
385
|
+
record.config = config;
|
|
386
|
+
record.cachedTheme = null;
|
|
387
|
+
scopeVersion += 1;
|
|
388
|
+
for (const listener of record.listeners) {
|
|
389
|
+
listener();
|
|
390
|
+
}
|
|
391
|
+
if (defaultScopeRecord() === record) {
|
|
392
|
+
publishDefaultScopeSnapshot();
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
dispose(): void {
|
|
397
|
+
if (record.disposed) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const wasDefault = defaultScopeRecord() === record;
|
|
401
|
+
record.disposed = true;
|
|
402
|
+
record.listeners.clear();
|
|
403
|
+
scopes.delete(record.id);
|
|
404
|
+
const index = scopeOrder.indexOf(record.id);
|
|
405
|
+
if (index >= 0) {
|
|
406
|
+
scopeOrder.splice(index, 1);
|
|
407
|
+
}
|
|
408
|
+
if (wasDefault) {
|
|
409
|
+
publishDefaultScopeSnapshot();
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
isDefault(): boolean {
|
|
414
|
+
return defaultScopeRecord() === record;
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Resolves a theme without registering a scope (one-shot resolution with
|
|
421
|
+
* identical semantics, including the contrast warning when any override is
|
|
422
|
+
* active — the legacy provider warned per instance). The React binding uses
|
|
423
|
+
* this for its synchronous render path, passing its hook-read inputs; tests
|
|
424
|
+
* use it for the parity matrix.
|
|
425
|
+
*/
|
|
426
|
+
export function resolveThemeForConfig(
|
|
427
|
+
config: ThemeScopeConfig = {},
|
|
428
|
+
inputs?: ThemeResolutionInputs,
|
|
429
|
+
): FacetTheme {
|
|
430
|
+
const { theme, hasCustomThemeOverride } = resolveScopeTheme(config, inputs);
|
|
431
|
+
validateScopeContrast(theme, hasCustomThemeOverride);
|
|
432
|
+
return theme;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Wire the agent-override input eagerly so an override write that lands
|
|
436
|
+
// before any scope/binding subscribes still bumps the store version (the
|
|
437
|
+
// legacy provider's useSyncExternalStore snapshot caught exactly this race).
|
|
438
|
+
ensureAgentOverrideSubscription();
|
|
439
|
+
|
|
440
|
+
/** Test-only: clears all scopes and global inputs back to initial state. */
|
|
441
|
+
export function _resetThemeStoreForTesting(): void {
|
|
442
|
+
scopes.clear();
|
|
443
|
+
scopeOrder.length = 0;
|
|
444
|
+
storeListeners.clear();
|
|
445
|
+
nextScopeId = 1;
|
|
446
|
+
systemAppearanceInput.set('light');
|
|
447
|
+
reducedMotionInput.set(false);
|
|
448
|
+
viewportWidthInput.set(390);
|
|
449
|
+
if (schemePreferenceStore.get() !== 'system') {
|
|
450
|
+
schemePreferenceStore.set('system');
|
|
451
|
+
}
|
|
452
|
+
if (agentOverrideSubscription) {
|
|
453
|
+
agentOverrideSubscription();
|
|
454
|
+
agentOverrideSubscription = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"lib": ["ES2022"],
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"paths": {
|
|
14
|
+
"@exact/core": ["../exact-core/src/index.ts"],
|
|
15
|
+
"@exact/core/*": ["../exact-core/src/*"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*", "facet-registry.json"]
|
|
19
|
+
}
|