@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,322 @@
|
|
|
1
|
+
// @system @ref LLP 0158 §6.2 — Design Mode D1: the facet overrides layer.
|
|
2
|
+
//
|
|
3
|
+
// Two process-global, file-backed override inputs plus the recipe-parameter
|
|
4
|
+
// resolver. This is the NEW resolution layer the round-1 review demanded be
|
|
5
|
+
// specified rather than assumed: theme overrides answer "what is
|
|
6
|
+
// theme.space.md"; THIS layer answers "what is FacetButton·primary's
|
|
7
|
+
// paddingX parameter before the recipe runs."
|
|
8
|
+
//
|
|
9
|
+
// Inputs are set by the app's generated authority files
|
|
10
|
+
// (js/src/theme.overrides.ts, js/src/facets.overrides.ts via their loader)
|
|
11
|
+
// and previewed in-memory by Design Mode; the theme store subscribes both
|
|
12
|
+
// and bumps its global version, so every binding re-renders on change.
|
|
13
|
+
// Precedence at parameter resolution time (LLP 0158 §6.2):
|
|
14
|
+
// override(variant) → override(base) → recipe default.
|
|
15
|
+
// A value is either a literal or a token rebind `{ token: "space.md" }` —
|
|
16
|
+
// the two stay distinct in source, provenance, and diffs.
|
|
17
|
+
|
|
18
|
+
import { resolveDensityPadding, type DeepPartial, type FacetTheme } from './internals.js';
|
|
19
|
+
import type { FacetThemeDefinition } from './theme-definition.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Recipe parameter declarations (LLP 0158 §6.2 "parameter reification")
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The tunable-parameter table. D1 reifies the state-independent geometry
|
|
27
|
+
* parameters of the interactive trio; color/state-aware parameters need a
|
|
28
|
+
* richer declaration (per-state values) and are deliberately deferred —
|
|
29
|
+
* color edits in D1 are theme-scope token edits. Keys are the facet
|
|
30
|
+
* component call names as they appear at Contract boundaries.
|
|
31
|
+
*
|
|
32
|
+
* LLP 0163 §5.4 (E1c): the AUTHORITY is the `params` sections in the
|
|
33
|
+
* facet components' own .contract files; this table is GENERATED output
|
|
34
|
+
* (bun run generate:facet-params), re-exported here so every consumer —
|
|
35
|
+
* the overrides authority's types (`FacetParamOverrides`), the resolver's
|
|
36
|
+
* call sites, the Design Mode panel, the prompt lane — keeps reading the
|
|
37
|
+
* same neutral data, indifferent to who wrote it. facet-core has no
|
|
38
|
+
* runtime import on facet-contract or the compiler: the dependency is
|
|
39
|
+
* generator-time data flow only.
|
|
40
|
+
*/
|
|
41
|
+
export { RECIPE_PARAMETERS } from './facet-params.generated.js';
|
|
42
|
+
import { RECIPE_PARAMETERS } from './facet-params.generated.js';
|
|
43
|
+
|
|
44
|
+
export type FacetParamComponent = keyof typeof RECIPE_PARAMETERS;
|
|
45
|
+
|
|
46
|
+
export type FacetParamValue = string | number | boolean | { token: string };
|
|
47
|
+
|
|
48
|
+
/** Shape of the facets.overrides.ts authority, derived from the table. */
|
|
49
|
+
export type FacetParamOverrides = {
|
|
50
|
+
[Component in FacetParamComponent]?: {
|
|
51
|
+
base?: Partial<Record<keyof (typeof RECIPE_PARAMETERS)[Component]['params'], FacetParamValue>>;
|
|
52
|
+
variants?: Record<
|
|
53
|
+
string,
|
|
54
|
+
Partial<Record<keyof (typeof RECIPE_PARAMETERS)[Component]['params'], FacetParamValue>>
|
|
55
|
+
>;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Process-global state
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/** Definition-level dial overrides (the theme.definition.ts authority, LLP
|
|
64
|
+
* 0269). This is the *generative* layer Design Mode dials and agents edit:
|
|
65
|
+
* accent, density, type, radius, motion. It resolves below every token
|
|
66
|
+
* override — a dial is broad, a token/instance override wins locally. */
|
|
67
|
+
export type ThemeDefinitionOverrides = DeepPartial<FacetThemeDefinition>;
|
|
68
|
+
|
|
69
|
+
interface FacetOverridesState {
|
|
70
|
+
themeDefinitionOverrides: ThemeDefinitionOverrides | null;
|
|
71
|
+
appThemeOverrides: DeepPartial<FacetTheme> | null;
|
|
72
|
+
facetParamOverrides: FacetParamOverrides | null;
|
|
73
|
+
listeners: Set<() => void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare global {
|
|
77
|
+
// eslint-disable-next-line no-var
|
|
78
|
+
var __exactFacetOverridesState: FacetOverridesState | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getState(): FacetOverridesState {
|
|
82
|
+
if (globalThis.__exactFacetOverridesState) {
|
|
83
|
+
return globalThis.__exactFacetOverridesState;
|
|
84
|
+
}
|
|
85
|
+
const state: FacetOverridesState = {
|
|
86
|
+
themeDefinitionOverrides: null,
|
|
87
|
+
appThemeOverrides: null,
|
|
88
|
+
facetParamOverrides: null,
|
|
89
|
+
listeners: new Set(),
|
|
90
|
+
};
|
|
91
|
+
globalThis.__exactFacetOverridesState = state;
|
|
92
|
+
return state;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function notify(): void {
|
|
96
|
+
for (const listener of getState().listeners) {
|
|
97
|
+
listener();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Definition-level dial overrides (the theme.definition.ts authority). */
|
|
102
|
+
export function setThemeDefinitionOverrides(overrides: ThemeDefinitionOverrides | null): void {
|
|
103
|
+
getState().themeDefinitionOverrides =
|
|
104
|
+
overrides && Object.keys(overrides).length > 0 ? overrides : null;
|
|
105
|
+
notify();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getThemeDefinitionOverrides(): ThemeDefinitionOverrides | null {
|
|
109
|
+
return getState().themeDefinitionOverrides;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** App-level theme token overrides (the theme.overrides.ts authority). */
|
|
113
|
+
export function setAppThemeOverrides(overrides: DeepPartial<FacetTheme> | null): void {
|
|
114
|
+
getState().appThemeOverrides = overrides && Object.keys(overrides).length > 0 ? overrides : null;
|
|
115
|
+
notify();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getAppThemeOverrides(): DeepPartial<FacetTheme> | null {
|
|
119
|
+
return getState().appThemeOverrides;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Facet recipe-parameter overrides (the facets.overrides.ts authority). */
|
|
123
|
+
export function setFacetParamOverrides(overrides: FacetParamOverrides | null): void {
|
|
124
|
+
getState().facetParamOverrides =
|
|
125
|
+
overrides && Object.keys(overrides).length > 0 ? overrides : null;
|
|
126
|
+
notify();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getFacetParamOverrides(): FacetParamOverrides | null {
|
|
130
|
+
return getState().facetParamOverrides;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One subscription covers both layers; the theme store bumps on either. */
|
|
134
|
+
export function subscribeFacetOverrides(listener: () => void): () => void {
|
|
135
|
+
const state = getState();
|
|
136
|
+
state.listeners.add(listener);
|
|
137
|
+
return () => {
|
|
138
|
+
state.listeners.delete(listener);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function _resetFacetOverridesForTests(): void {
|
|
143
|
+
const state = getState();
|
|
144
|
+
state.themeDefinitionOverrides = null;
|
|
145
|
+
state.appThemeOverrides = null;
|
|
146
|
+
state.facetParamOverrides = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Resolution
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
function readTokenPath(theme: FacetTheme, path: string): unknown {
|
|
154
|
+
let cursor: unknown = theme;
|
|
155
|
+
for (const segment of path.split('.')) {
|
|
156
|
+
if (cursor === null || typeof cursor !== 'object') {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
160
|
+
}
|
|
161
|
+
return cursor;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Value kind of a theme token leaf (LLP 0166 §4.4 schema kinds). */
|
|
165
|
+
export type ThemeTokenValueKind = 'dimension' | 'color' | 'duration' | 'number' | 'string' | 'boolean';
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Flattens a theme into the token schema `$` references validate against
|
|
169
|
+
* (LLP 0166 §4.4): every leaf path mapped to its value kind. Paths name
|
|
170
|
+
* schema positions, not concrete values — any complete theme yields the
|
|
171
|
+
* same path set, so callers typically pass a base theme.
|
|
172
|
+
*/
|
|
173
|
+
export function collectThemeTokenSchema(theme: FacetTheme): {
|
|
174
|
+
paths: Record<string, ThemeTokenValueKind>;
|
|
175
|
+
} {
|
|
176
|
+
const paths: Record<string, ThemeTokenValueKind> = {};
|
|
177
|
+
const DURATION_LEAVES = new Set(['fast', 'normal', 'slow']);
|
|
178
|
+
const kindFor = (rootKey: string, leafKey: string, value: unknown): ThemeTokenValueKind => {
|
|
179
|
+
if (rootKey === 'color' || leafKey === 'color') {
|
|
180
|
+
return 'color';
|
|
181
|
+
}
|
|
182
|
+
if (rootKey === 'space' || rootKey === 'radius') {
|
|
183
|
+
return 'dimension';
|
|
184
|
+
}
|
|
185
|
+
if (rootKey === 'density') {
|
|
186
|
+
return leafKey === 'paddingScale'
|
|
187
|
+
? 'number'
|
|
188
|
+
: typeof value === 'number'
|
|
189
|
+
? 'dimension'
|
|
190
|
+
: typeof value === 'boolean'
|
|
191
|
+
? 'boolean'
|
|
192
|
+
: 'string';
|
|
193
|
+
}
|
|
194
|
+
if (rootKey === 'type') {
|
|
195
|
+
if (leafKey === 'fontWeight' || leafKey === 'letterSpacing') {
|
|
196
|
+
return 'number';
|
|
197
|
+
}
|
|
198
|
+
return typeof value === 'number' ? 'dimension' : 'string';
|
|
199
|
+
}
|
|
200
|
+
if (rootKey === 'elevation') {
|
|
201
|
+
return typeof value === 'number' ? 'dimension' : 'string';
|
|
202
|
+
}
|
|
203
|
+
if (rootKey === 'motion' && typeof value === 'number') {
|
|
204
|
+
return DURATION_LEAVES.has(leafKey) ? 'duration' : 'number';
|
|
205
|
+
}
|
|
206
|
+
if (typeof value === 'number') {
|
|
207
|
+
return 'number';
|
|
208
|
+
}
|
|
209
|
+
if (typeof value === 'boolean') {
|
|
210
|
+
return 'boolean';
|
|
211
|
+
}
|
|
212
|
+
return 'string';
|
|
213
|
+
};
|
|
214
|
+
const walk = (value: unknown, prefix: string, rootKey: string): void => {
|
|
215
|
+
// Arrays (e.g. type.*.fontFamily stacks) are token leaves, not branches.
|
|
216
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
217
|
+
Object.keys(value as Record<string, unknown>).forEach((key) => {
|
|
218
|
+
walk(
|
|
219
|
+
(value as Record<string, unknown>)[key],
|
|
220
|
+
prefix === '' ? key : `${prefix}.${key}`,
|
|
221
|
+
prefix === '' ? key : rootKey,
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const leafKey = prefix.split('.').pop() ?? prefix;
|
|
227
|
+
paths[prefix] = kindFor(rootKey, leafKey, value);
|
|
228
|
+
};
|
|
229
|
+
const roots: Array<keyof FacetTheme> = [
|
|
230
|
+
'color',
|
|
231
|
+
'space',
|
|
232
|
+
'density',
|
|
233
|
+
'type',
|
|
234
|
+
'radius',
|
|
235
|
+
'elevation',
|
|
236
|
+
'zIndex',
|
|
237
|
+
'motion',
|
|
238
|
+
'adaptation',
|
|
239
|
+
];
|
|
240
|
+
roots.forEach((root) => {
|
|
241
|
+
walk(theme[root], root, root);
|
|
242
|
+
});
|
|
243
|
+
return { paths };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resolves one recipe parameter: override(variant) → override(base) →
|
|
248
|
+
* `fallback` (the recipe's existing expression, so empty overrides are
|
|
249
|
+
* byte-identical to pre-reification output). Token rebinds read through the
|
|
250
|
+
* live theme — including the sentinel tracing twin, so traced origins stay
|
|
251
|
+
* correct under rebinding for free.
|
|
252
|
+
*/
|
|
253
|
+
/**
|
|
254
|
+
* Button padding recipe (LLP 0269 §5, D10): the density-scaled space defaults
|
|
255
|
+
* behind the LLP 0158 §6.2 override layer. Both bindings (React `Button`,
|
|
256
|
+
* facet-contract `buttonStyle`) call this one helper, so the density dial and
|
|
257
|
+
* the override layer cannot drift per framework. An explicit override is an
|
|
258
|
+
* absolute author value and is NOT re-scaled by density (most-specific-wins:
|
|
259
|
+
* the override already states the final geometry).
|
|
260
|
+
*/
|
|
261
|
+
export function resolveButtonPadding(
|
|
262
|
+
theme: FacetTheme,
|
|
263
|
+
variant: string | null | undefined,
|
|
264
|
+
): { paddingX: number; paddingY: number } {
|
|
265
|
+
return {
|
|
266
|
+
paddingX: resolveRecipeParam(
|
|
267
|
+
theme,
|
|
268
|
+
'FacetButton',
|
|
269
|
+
variant ?? null,
|
|
270
|
+
'paddingX',
|
|
271
|
+
resolveDensityPadding(theme, theme.space.md),
|
|
272
|
+
),
|
|
273
|
+
paddingY: resolveRecipeParam(
|
|
274
|
+
theme,
|
|
275
|
+
'FacetButton',
|
|
276
|
+
variant ?? null,
|
|
277
|
+
'paddingY',
|
|
278
|
+
resolveDensityPadding(theme, theme.space.xs),
|
|
279
|
+
),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Card padding recipe (LLP 0269 §5, D10) — same contract as
|
|
284
|
+
* `resolveButtonPadding`: density-scaled default, absolute override wins. */
|
|
285
|
+
export function resolveCardPadding(theme: FacetTheme): number {
|
|
286
|
+
return resolveRecipeParam(
|
|
287
|
+
theme,
|
|
288
|
+
'FacetCard',
|
|
289
|
+
null,
|
|
290
|
+
'padding',
|
|
291
|
+
resolveDensityPadding(theme, theme.space.lg),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function resolveRecipeParam<T>(
|
|
296
|
+
theme: FacetTheme,
|
|
297
|
+
component: string,
|
|
298
|
+
variant: string | null | undefined,
|
|
299
|
+
param: string,
|
|
300
|
+
fallback: T,
|
|
301
|
+
): T {
|
|
302
|
+
const overrides = getState().facetParamOverrides?.[component as FacetParamComponent];
|
|
303
|
+
if (!overrides) {
|
|
304
|
+
return fallback;
|
|
305
|
+
}
|
|
306
|
+
const fromVariant =
|
|
307
|
+
variant != null
|
|
308
|
+
? (overrides.variants?.[variant] as Record<string, FacetParamValue> | undefined)?.[param]
|
|
309
|
+
: undefined;
|
|
310
|
+
const value =
|
|
311
|
+
fromVariant !== undefined
|
|
312
|
+
? fromVariant
|
|
313
|
+
: (overrides.base as Record<string, FacetParamValue> | undefined)?.[param];
|
|
314
|
+
if (value === undefined) {
|
|
315
|
+
return fallback;
|
|
316
|
+
}
|
|
317
|
+
if (value !== null && typeof value === 'object' && 'token' in value) {
|
|
318
|
+
const resolved = readTokenPath(theme, value.token);
|
|
319
|
+
return resolved === undefined ? fallback : (resolved as T);
|
|
320
|
+
}
|
|
321
|
+
return value as T;
|
|
322
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// AUTO-GENERATED — DO NOT EDIT.
|
|
2
|
+
// AUTHORITY: the `params` sections in
|
|
3
|
+
// packages/exact-facet-contract/src/components/*.contract (LLP 0163 §5.4).
|
|
4
|
+
// Generator: bun run generate:facet-params (scripts/generate-facet-params.mjs).
|
|
5
|
+
//
|
|
6
|
+
// The reified recipe-parameter table (LLP 0158 §6.2): the overrides
|
|
7
|
+
// authority's types derive from it, resolveRecipeParam's callers consult
|
|
8
|
+
// it, the Design Mode panel enumerates it, and the prompt lane validates
|
|
9
|
+
// against it. It is DERIVED OUTPUT — change the params sections and
|
|
10
|
+
// regenerate; never hand-edit (verified by design-provenance-fixtures).
|
|
11
|
+
|
|
12
|
+
export const RECIPE_PARAMETERS = {
|
|
13
|
+
FacetBadge: {
|
|
14
|
+
variants: [
|
|
15
|
+
'neutral',
|
|
16
|
+
'accent',
|
|
17
|
+
'success',
|
|
18
|
+
'danger'
|
|
19
|
+
],
|
|
20
|
+
params: {
|
|
21
|
+
radius: {
|
|
22
|
+
defaultToken: 'radius.full',
|
|
23
|
+
kind: 'dimension'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
FacetButton: {
|
|
28
|
+
variants: [
|
|
29
|
+
'primary',
|
|
30
|
+
'secondary',
|
|
31
|
+
'ghost'
|
|
32
|
+
],
|
|
33
|
+
params: {
|
|
34
|
+
paddingX: {
|
|
35
|
+
defaultToken: 'space.md',
|
|
36
|
+
kind: 'dimension'
|
|
37
|
+
},
|
|
38
|
+
paddingY: {
|
|
39
|
+
defaultToken: 'space.xs',
|
|
40
|
+
kind: 'dimension'
|
|
41
|
+
},
|
|
42
|
+
radius: {
|
|
43
|
+
defaultToken: 'radius.element',
|
|
44
|
+
kind: 'dimension'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
FacetCard: {
|
|
49
|
+
variants: [],
|
|
50
|
+
params: {
|
|
51
|
+
padding: {
|
|
52
|
+
defaultToken: 'space.lg',
|
|
53
|
+
kind: 'dimension'
|
|
54
|
+
},
|
|
55
|
+
gap: {
|
|
56
|
+
defaultToken: 'space.md',
|
|
57
|
+
kind: 'dimension'
|
|
58
|
+
},
|
|
59
|
+
radius: {
|
|
60
|
+
defaultToken: 'radius.container',
|
|
61
|
+
kind: 'dimension'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} as const;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type FacetScoreValue = 0 | 1 | 2 | 3;
|
|
2
|
+
|
|
3
|
+
export interface FacetComponentScorecard {
|
|
4
|
+
animation: FacetScoreValue;
|
|
5
|
+
interaction: FacetScoreValue;
|
|
6
|
+
accessibility: FacetScoreValue;
|
|
7
|
+
testing: FacetScoreValue;
|
|
8
|
+
notes: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function scorecard(
|
|
12
|
+
animation: FacetScoreValue,
|
|
13
|
+
interaction: FacetScoreValue,
|
|
14
|
+
accessibility: FacetScoreValue,
|
|
15
|
+
testing: FacetScoreValue,
|
|
16
|
+
notes: string,
|
|
17
|
+
): FacetComponentScorecard {
|
|
18
|
+
return {
|
|
19
|
+
animation,
|
|
20
|
+
interaction,
|
|
21
|
+
accessibility,
|
|
22
|
+
testing,
|
|
23
|
+
notes,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const facetScorecards: Record<string, FacetComponentScorecard> = {
|
|
28
|
+
Button: scorecard(3, 3, 3, 3, 'Primary pathfinder component with dedicated tree, interaction, and screenshot coverage.'),
|
|
29
|
+
Input: scorecard(3, 3, 3, 3, 'Shared input chrome has hover, focus, validation, reduced-motion, and regression coverage.'),
|
|
30
|
+
Textarea: scorecard(3, 3, 3, 3, 'Textarea rides the same shell and tests as Input, including FormField wiring.'),
|
|
31
|
+
FormField: scorecard(1, 2, 3, 3, 'Static wrapper, but labels, descriptions, messages, and aria wiring are covered in the form matrix.'),
|
|
32
|
+
Card: scorecard(3, 3, 2, 3, 'Interactive cards now carry hover, focus, and press treatment with tree and screenshot checks.'),
|
|
33
|
+
Badge: scorecard(3, 3, 2, 3, 'Interactive badges share the control motion surface and have dedicated interaction assertions.'),
|
|
34
|
+
Label: scorecard(0, 1, 3, 2, 'Mostly semantic text, but it now participates in the shared form-field contract.'),
|
|
35
|
+
Separator: scorecard(0, 0, 2, 2, 'Pure presentational divider with basic catalog and tree coverage.'),
|
|
36
|
+
Avatar: scorecard(1, 0, 2, 2, 'Static feedback primitive with catalog and fixture coverage, but no richer interaction path.'),
|
|
37
|
+
Skeleton: scorecard(1, 0, 1, 2, 'Static loading placeholder with catalog coverage; no interaction surface.'),
|
|
38
|
+
Progress: scorecard(2, 0, 2, 2, 'Determinate progress has semantics and catalog coverage, but no dedicated interaction path.'),
|
|
39
|
+
Spinner: scorecard(2, 0, 2, 2, 'Indeterminate loading state is visually covered but intentionally non-interactive.'),
|
|
40
|
+
Alert: scorecard(2, 0, 3, 2, 'Live-region alert surface is semantically strong and visible in the catalog/fixture pages.'),
|
|
41
|
+
Toggle: scorecard(3, 3, 3, 3, 'Toggle now has full interaction-state coverage plus reduced-motion and tree assertions.'),
|
|
42
|
+
Checkbox: scorecard(3, 3, 3, 3, 'Checkbox carries hover, focus, press, and checked states with dedicated regression tests.'),
|
|
43
|
+
Slider: scorecard(2, 2, 3, 2, 'Accessible stepper fallback exists, but the composed fallback still lacks a richer drag-path test story.'),
|
|
44
|
+
RadioGroup: scorecard(3, 3, 3, 3, 'Roving focus, selection motion, and interaction-state coverage are in place.'),
|
|
45
|
+
Dialog: scorecard(3, 3, 3, 3, 'Dialog has presence-backed motion, focus behavior, and dedicated screenshot/tree coverage.'),
|
|
46
|
+
AlertDialog: scorecard(3, 2, 3, 2, 'Modal semantics are strong, but the screenshot matrix only covers it through the shared fixture surface today.'),
|
|
47
|
+
Sheet: scorecard(3, 2, 3, 2, 'Sheet reuses the dialog stack and motion surface, with baseline catalog coverage but fewer dedicated checks.'),
|
|
48
|
+
Accordion: scorecard(3, 3, 3, 3, 'Presence-backed content, trigger interaction states, and dedicated tree/screenshot tests are in place.'),
|
|
49
|
+
Collapsible: scorecard(3, 3, 3, 3, 'Standalone disclosure primitive ships with presence motion, interaction states, and dedicated behavior plus visual coverage.'),
|
|
50
|
+
Tabs: scorecard(3, 3, 3, 3, 'Animated indicator, focus states, and dedicated tree/screenshot coverage are in place.'),
|
|
51
|
+
Toast: scorecard(3, 2, 3, 3, 'Toast has presence-backed motion, semantics, and dedicated visual/tree regression coverage.'),
|
|
52
|
+
Popover: scorecard(3, 3, 2, 3, 'Floating placement and presence motion are covered in both interaction and screenshot tests.'),
|
|
53
|
+
Tooltip: scorecard(3, 2, 2, 2, 'Tooltip motion is polished, but focus/hover and screenshot coverage are still lighter than Dialog/Popover.'),
|
|
54
|
+
DropdownMenu: scorecard(3, 3, 2, 2, 'Menu behavior exists, but the visual regression surface is still mostly catalog-level.'),
|
|
55
|
+
Select: scorecard(3, 3, 3, 3, 'Select now has its own trigger path, value state, and both interaction and screenshot coverage.'),
|
|
56
|
+
Combobox: scorecard(3, 3, 3, 3, 'Combobox adds searchable filtering, keyboard selection, and dedicated regression coverage on top of the floating listbox stack.'),
|
|
57
|
+
Command: scorecard(3, 3, 3, 3, 'Command ships as a dialog-backed palette with grouped filtering, keyboard selection, and dedicated visual plus interaction coverage.'),
|
|
58
|
+
Table: scorecard(3, 3, 3, 3, 'Table now provides semantic row and header structure, sortable headers, interactive rows, and dedicated coverage.'),
|
|
59
|
+
ContextMenu: scorecard(3, 2, 2, 2, 'Context menu behavior exists, but its visual and accessibility checks are still less exhaustive than top-level menus.'),
|
|
60
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @system @ref LLP 0157 — @exact/facet-core public surface.
|
|
2
|
+
//
|
|
3
|
+
// Framework-neutral heart of the Facet design system: theme types and
|
|
4
|
+
// definitions, every variant recipe, motion builders, color and control
|
|
5
|
+
// math, catalog/scorecard metadata, and the consolidated theme store.
|
|
6
|
+
// `@exact/facet` (React) and `@exact/facet-contract` (Contract) are thin
|
|
7
|
+
// bindings over this package.
|
|
8
|
+
|
|
9
|
+
export * from './internals.js';
|
|
10
|
+
export * from './theme-definition.js';
|
|
11
|
+
export * from './facet-catalog.js';
|
|
12
|
+
export * from './facet-scorecard.js';
|
|
13
|
+
export * from './theme-store.js';
|
|
14
|
+
export * from './store-machinery.js';
|
|
15
|
+
export * from './provenance-trace.js';
|
|
16
|
+
export * from './facet-overrides.js';
|