@multiplatform.one/theme 6.1.0 → 6.4.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/README.md +73 -0
- package/package.json +22 -9
- package/src/dates.spec.ts +75 -0
- package/src/dates.ts +130 -0
- package/src/devWarn.spec.ts +144 -0
- package/src/devWarn.ts +235 -0
- package/src/figma/figmaTokens.spec.ts +244 -0
- package/src/figma/figmaTokens.ts +503 -0
- package/src/font.ts +6 -8
- package/src/fonts/createFontLoader.web.ts +15 -4
- package/src/index.ts +7 -0
- package/src/keyboardFocusRing.ts +45 -0
- package/src/menuRow.spec.ts +32 -0
- package/src/menuRow.ts +31 -0
- package/src/theme/FontKnobStyles.native.tsx +24 -0
- package/src/theme/FontKnobStyles.tsx +256 -0
- package/src/theme/animations/reactNative.ts +6 -1
- package/src/theme/chartPalette.spec.ts +159 -0
- package/src/theme/chartPalette.ts +170 -0
- package/src/theme/colorRules.ts +33 -1
- package/src/theme/cookies.spec.ts +32 -0
- package/src/theme/cookiesPersistence.spec.ts +86 -0
- package/src/theme/cornerSmoothing.native.ts +16 -0
- package/src/theme/cornerSmoothing.spec.ts +27 -0
- package/src/theme/cornerSmoothing.ts +46 -0
- package/src/theme/createDefaultThemeConfig.ts +32 -8
- package/src/theme/createThemeConfig.spec.ts +137 -0
- package/src/theme/createThemes.ts +77 -2
- package/src/theme/defaults/accent.ts +39 -0
- package/src/theme/defaults/base.ts +63 -0
- package/src/theme/defaults/builderOptions.ts +174 -0
- package/src/theme/defaults/fonts.spec.ts +87 -0
- package/src/theme/defaults/fonts.ts +89 -0
- package/src/theme/defaults/index.ts +4 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +196 -2
- package/src/theme/devtools/buildSnapshot.ts +11 -0
- package/src/theme/devtools/knobIcons.tsx +141 -0
- package/src/theme/devtools/vizConstants.ts +1 -1
- package/src/theme/focusState.spec.ts +174 -0
- package/src/theme/focusState.ts +112 -0
- package/src/theme/fontKnobStyles.spec.ts +98 -0
- package/src/theme/hairline.native.ts +43 -0
- package/src/theme/hairline.spec.ts +63 -0
- package/src/theme/hairline.ts +101 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/intent.spec.tsx +2 -2
- package/src/theme/intents.ts +6 -2
- package/src/theme/knobs.ts +134 -2
- package/src/theme/layoutTokens.spec.ts +75 -0
- package/src/theme/layoutTokens.ts +213 -0
- package/src/theme/layoutTokensHooks.spec.tsx +157 -0
- package/src/theme/presets.spec.ts +17 -0
- package/src/theme/presets.ts +22 -2
- package/src/theme/readableColor.ts +101 -0
- package/src/theme/recipes.ts +130 -7
- package/src/theme/resolveKnobs.spec.ts +560 -21
- package/src/theme/resolveKnobs.ts +267 -23
- package/src/theme/shared.tsx +26 -2
- package/src/theme/useResolvedKnobs.ts +67 -26
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +166 -0
- package/types/audit/constraintAudit.d.ts +83 -0
- package/types/audit/constraintAudit.d.ts.map +1 -0
- package/types/audit/diffReport.d.ts +45 -0
- package/types/audit/diffReport.d.ts.map +1 -0
- package/types/audit/index.d.ts +5 -0
- package/types/audit/index.d.ts.map +1 -0
- package/types/dates.d.ts +52 -0
- package/types/dates.d.ts.map +1 -0
- package/types/devWarn.d.ts +104 -0
- package/types/devWarn.d.ts.map +1 -0
- package/types/figma/figmaTokens.d.ts +168 -0
- package/types/figma/figmaTokens.d.ts.map +1 -0
- package/types/font.d.ts +5 -0
- package/types/font.d.ts.map +1 -0
- package/types/fonts/createFontLoader.d.ts +3 -0
- package/types/fonts/createFontLoader.d.ts.map +1 -0
- package/types/fonts/createFontLoader.web.d.ts +3 -0
- package/types/fonts/createFontLoader.web.d.ts.map +1 -0
- package/types/fonts/index.d.ts +3 -0
- package/types/fonts/index.d.ts.map +1 -0
- package/types/fonts/types.d.ts +12 -0
- package/types/fonts/types.d.ts.map +1 -0
- package/types/index.d.ts +9 -0
- package/types/index.d.ts.map +1 -0
- package/types/keyboardFocusRing.d.ts +32 -0
- package/types/keyboardFocusRing.d.ts.map +1 -0
- package/types/menuRow.d.ts +32 -0
- package/types/menuRow.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.d.ts +49 -0
- package/types/theme/FontKnobStyles.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.native.d.ts +12 -0
- package/types/theme/FontKnobStyles.native.d.ts.map +1 -0
- package/types/theme/Intent.d.ts +20 -0
- package/types/theme/Intent.d.ts.map +1 -0
- package/types/theme/Preset.d.ts +15 -0
- package/types/theme/Preset.d.ts.map +1 -0
- package/types/theme/PresetContext.d.ts +18 -0
- package/types/theme/PresetContext.d.ts.map +1 -0
- package/types/theme/Tint.d.ts +19 -0
- package/types/theme/Tint.d.ts.map +1 -0
- package/types/theme/animations/css.d.ts +24 -0
- package/types/theme/animations/css.d.ts.map +1 -0
- package/types/theme/animations/index.d.ts +2 -0
- package/types/theme/animations/index.d.ts.map +1 -0
- package/types/theme/animations/index.web.d.ts +2 -0
- package/types/theme/animations/index.web.d.ts.map +1 -0
- package/types/theme/animations/reactNative.d.ts +104 -0
- package/types/theme/animations/reactNative.d.ts.map +1 -0
- package/types/theme/chartPalette.d.ts +62 -0
- package/types/theme/chartPalette.d.ts.map +1 -0
- package/types/theme/colorRules.d.ts +69 -0
- package/types/theme/colorRules.d.ts.map +1 -0
- package/types/theme/cookies.d.ts +76 -0
- package/types/theme/cookies.d.ts.map +1 -0
- package/types/theme/cornerSmoothing.d.ts +31 -0
- package/types/theme/cornerSmoothing.d.ts.map +1 -0
- package/types/theme/cornerSmoothing.native.d.ts +14 -0
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -0
- package/types/theme/createDefaultThemeConfig.d.ts +37 -0
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -0
- package/types/theme/createThemes.d.ts +38 -0
- package/types/theme/createThemes.d.ts.map +1 -0
- package/types/theme/defaults/accent.d.ts +3 -0
- package/types/theme/defaults/accent.d.ts.map +1 -0
- package/types/theme/defaults/base.d.ts +3 -0
- package/types/theme/defaults/base.d.ts.map +1 -0
- package/types/theme/defaults/builderOptions.d.ts +3 -0
- package/types/theme/defaults/builderOptions.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +7 -0
- package/types/theme/defaults/fonts.d.ts.map +1 -0
- package/types/theme/defaults/index.d.ts +5 -0
- package/types/theme/defaults/index.d.ts.map +1 -0
- package/types/theme/devtools/ColorLineVisualizer.d.ts +17 -0
- package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -0
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +87 -0
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -0
- package/types/theme/devtools/buildSnapshot.d.ts +12 -0
- package/types/theme/devtools/buildSnapshot.d.ts.map +1 -0
- package/types/theme/devtools/knobIcons.d.ts +33 -0
- package/types/theme/devtools/knobIcons.d.ts.map +1 -0
- package/types/theme/devtools/vizConstants.d.ts +5 -0
- package/types/theme/devtools/vizConstants.d.ts.map +1 -0
- package/types/theme/focusState.d.ts +42 -0
- package/types/theme/focusState.d.ts.map +1 -0
- package/types/theme/hairline.d.ts +88 -0
- package/types/theme/hairline.d.ts.map +1 -0
- package/types/theme/hairline.native.d.ts +42 -0
- package/types/theme/hairline.native.d.ts.map +1 -0
- package/types/theme/index.d.ts +29 -0
- package/types/theme/index.d.ts.map +1 -0
- package/types/theme/intents.d.ts +3 -0
- package/types/theme/intents.d.ts.map +1 -0
- package/types/theme/knobs.d.ts +236 -0
- package/types/theme/knobs.d.ts.map +1 -0
- package/types/theme/layoutTokens.d.ts +104 -0
- package/types/theme/layoutTokens.d.ts.map +1 -0
- package/types/theme/preset.types.d.ts +21 -0
- package/types/theme/preset.types.d.ts.map +1 -0
- package/types/theme/presets.d.ts +4 -0
- package/types/theme/presets.d.ts.map +1 -0
- package/types/theme/readableColor.d.ts +17 -0
- package/types/theme/readableColor.d.ts.map +1 -0
- package/types/theme/recipes.d.ts +200 -0
- package/types/theme/recipes.d.ts.map +1 -0
- package/types/theme/resolveKnobs.d.ts +22 -0
- package/types/theme/resolveKnobs.d.ts.map +1 -0
- package/types/theme/shared.d.ts +40 -0
- package/types/theme/shared.d.ts.map +1 -0
- package/types/theme/theme.d.ts +19 -0
- package/types/theme/theme.d.ts.map +1 -0
- package/types/theme/theme.native.d.ts +10 -0
- package/types/theme/theme.native.d.ts.map +1 -0
- package/types/theme/useColorScale.d.ts +7 -0
- package/types/theme/useColorScale.d.ts.map +1 -0
- package/types/theme/useColorScale.native.d.ts +6 -0
- package/types/theme/useColorScale.native.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts +32 -0
- package/types/theme/useResolvedKnobs.d.ts.map +1 -0
- package/types/theme/useTheme.d.ts +31 -0
- package/types/theme/useTheme.d.ts.map +1 -0
- package/types/theme/useTheme.native.d.ts +10 -0
- package/types/theme/useTheme.native.d.ts.map +1 -0
- package/CHANGELOG.md +0 -53
- package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +0 -69
- package/src/theme/pokemonScreen.spec.ts +0 -109
- package/tsconfig.json +0 -11
- package/vitest.config.mjs +0 -9
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
defaultKnobs,
|
|
4
|
+
emptyStateKnobs,
|
|
5
|
+
Space,
|
|
6
|
+
Size,
|
|
7
|
+
Density,
|
|
8
|
+
CornerSmoothing,
|
|
9
|
+
FieldLabelPlacement,
|
|
10
|
+
RequiredMarking,
|
|
11
|
+
TableZebra,
|
|
12
|
+
BulkBarPlacement,
|
|
13
|
+
SelectAllScope,
|
|
14
|
+
TimestampStyle,
|
|
15
|
+
DisabledStyle,
|
|
16
|
+
FormAutofocus,
|
|
17
|
+
} from "./knobs";
|
|
3
18
|
import type { Knobs } from "./knobs";
|
|
4
19
|
import type { KnobProps } from "./recipes";
|
|
5
|
-
import {
|
|
20
|
+
import { cornerSmoothClassName } from "./cornerSmoothing";
|
|
21
|
+
import { defaultPreset, boldPreset } from "./presets";
|
|
22
|
+
import { applyDensity, capContainerRadius, resolveKnobs } from "./resolveKnobs";
|
|
6
23
|
import {
|
|
7
24
|
normalizeToHex,
|
|
8
25
|
relativeLuminance,
|
|
@@ -18,8 +35,12 @@ function knobsWith(overrides: Partial<Knobs>): Knobs {
|
|
|
18
35
|
const expectedKnobPropKeys: (keyof KnobProps)[] = [
|
|
19
36
|
"borderRadius",
|
|
20
37
|
"borderRadiusOuter",
|
|
38
|
+
"containerRadius",
|
|
21
39
|
"elevation",
|
|
22
40
|
"surface",
|
|
41
|
+
"cardSurface",
|
|
42
|
+
"elevatedSurface",
|
|
43
|
+
"featureSurface",
|
|
23
44
|
"inputSurface",
|
|
24
45
|
"panelPadding",
|
|
25
46
|
"gap",
|
|
@@ -36,6 +57,15 @@ const expectedKnobPropKeys: (keyof KnobProps)[] = [
|
|
|
36
57
|
"inputBackground",
|
|
37
58
|
"space",
|
|
38
59
|
"size",
|
|
60
|
+
"density",
|
|
61
|
+
"fieldLabelPlacement",
|
|
62
|
+
"requiredMarking",
|
|
63
|
+
"tableZebra",
|
|
64
|
+
"bulkBarPlacement",
|
|
65
|
+
"selectAllScope",
|
|
66
|
+
"timestampStyle",
|
|
67
|
+
"disabledStyle",
|
|
68
|
+
"formAutofocus",
|
|
39
69
|
];
|
|
40
70
|
|
|
41
71
|
// ── colorRules unit tests ────────────────────────────────────
|
|
@@ -190,6 +220,33 @@ describe("resolveKnobs", () => {
|
|
|
190
220
|
});
|
|
191
221
|
});
|
|
192
222
|
|
|
223
|
+
// ── elevation on NATIVE (SB-N-04) ──────────────────────────
|
|
224
|
+
// Android's RCTView `elevation` prop is a Double; a token string reaching a
|
|
225
|
+
// plain RN View kills the ReactHost. Native must emit the numeric
|
|
226
|
+
// tokens.size values Tamagui would resolve those tokens to.
|
|
227
|
+
|
|
228
|
+
describe("elevation knob (native)", () => {
|
|
229
|
+
it("emits RN-safe numbers, never token strings", async () => {
|
|
230
|
+
vi.resetModules();
|
|
231
|
+
vi.doMock("@multiplatform.one/platform", () => ({ isWeb: false }));
|
|
232
|
+
const { resolveKnobs: resolveNative } = await import("./resolveKnobs");
|
|
233
|
+
try {
|
|
234
|
+
expect(resolveNative(knobsWith({ elevation: "none" })).knobProps.elevation).toBeUndefined();
|
|
235
|
+
const small = resolveNative(
|
|
236
|
+
knobsWith({ elevation: "small", hover: { elevation: "medium" } }),
|
|
237
|
+
);
|
|
238
|
+
expect(small.knobProps.elevation).toBe(20);
|
|
239
|
+
expect(small.knobProps.elevatedSurface.elevation).toBe(20);
|
|
240
|
+
expect(small.elevation.hoverKnobProps?.elevation).toBe(28);
|
|
241
|
+
expect(resolveNative(knobsWith({ elevation: "medium" })).knobProps.elevation).toBe(28);
|
|
242
|
+
expect(resolveNative(knobsWith({ elevation: "large" })).knobProps.elevation).toBe(44);
|
|
243
|
+
} finally {
|
|
244
|
+
vi.doUnmock("@multiplatform.one/platform");
|
|
245
|
+
vi.resetModules();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
193
250
|
// ── interaction state overrides ──────────────────────────────
|
|
194
251
|
|
|
195
252
|
describe("interaction state overrides", () => {
|
|
@@ -213,12 +270,16 @@ describe("resolveKnobs", () => {
|
|
|
213
270
|
expect(control.hoverKnobProps?.borderRadius).toBe("$12");
|
|
214
271
|
});
|
|
215
272
|
|
|
216
|
-
it("empty state
|
|
273
|
+
it("empty state knobs still emit default hover/press layers + focus ring (W12)", () => {
|
|
217
274
|
const { control } = resolveKnobs(defaultKnobs);
|
|
218
|
-
expect(control.hoverKnobProps).
|
|
219
|
-
expect(control.pressKnobProps).
|
|
275
|
+
expect(control.hoverKnobProps).toEqual({ backgroundColor: "$backgroundHover" });
|
|
276
|
+
expect(control.pressKnobProps).toEqual({ backgroundColor: "$backgroundPress" });
|
|
220
277
|
expect(control.focusKnobProps).toBeUndefined();
|
|
221
|
-
expect(control.focusVisibleKnobProps).
|
|
278
|
+
expect(control.focusVisibleKnobProps).toMatchObject({
|
|
279
|
+
outlineWidth: 2,
|
|
280
|
+
outlineStyle: "solid",
|
|
281
|
+
outlineColor: "$outlineColor",
|
|
282
|
+
});
|
|
222
283
|
});
|
|
223
284
|
|
|
224
285
|
it("focus.elevation maps to elevation.focusKnobProps.elevation", () => {
|
|
@@ -240,9 +301,9 @@ describe("resolveKnobs", () => {
|
|
|
240
301
|
expect(knobProps.panelPadding.padding).toBe("$3");
|
|
241
302
|
});
|
|
242
303
|
|
|
243
|
-
it('maps gap "large" → "$
|
|
304
|
+
it('maps gap "large" → "$5"', () => {
|
|
244
305
|
const { knobProps } = resolveKnobs(knobsWith({ space: "large" }));
|
|
245
|
-
expect(knobProps.gap.gap).toBe("$
|
|
306
|
+
expect(knobProps.gap.gap).toBe("$5");
|
|
246
307
|
});
|
|
247
308
|
|
|
248
309
|
it("passes space value through to knobProps", () => {
|
|
@@ -262,10 +323,10 @@ describe("resolveKnobs", () => {
|
|
|
262
323
|
expect(knobProps.panelPadding.padding).toBe("$3");
|
|
263
324
|
});
|
|
264
325
|
|
|
265
|
-
// T2.1c — space "large" still maps gapLg → "$
|
|
266
|
-
it('space "large" maps gapLg → "$
|
|
326
|
+
// T2.1c — space "large" still maps gapLg → "$6" regardless of size
|
|
327
|
+
it('space "large" maps gapLg → "$6" regardless of size', () => {
|
|
267
328
|
const { knobProps } = resolveKnobs(knobsWith({ space: "large", size: "small" }));
|
|
268
|
-
expect(knobProps.gapLg.gap).toBe("$
|
|
329
|
+
expect(knobProps.gapLg.gap).toBe("$6");
|
|
269
330
|
});
|
|
270
331
|
});
|
|
271
332
|
|
|
@@ -314,9 +375,256 @@ describe("resolveKnobs", () => {
|
|
|
314
375
|
it("default knobs (size+space both medium) produce consistent output", () => {
|
|
315
376
|
const resolved = resolveKnobs(defaultKnobs);
|
|
316
377
|
expect(resolved.knobProps.sizeToken).toBe("$4");
|
|
317
|
-
expect(resolved.knobProps.gap.gap).toBe("$
|
|
378
|
+
expect(resolved.knobProps.gap.gap).toBe("$4");
|
|
318
379
|
expect(resolved.knobProps.panelPadding.padding).toBe("$4");
|
|
319
|
-
expect(resolved.knobProps.gapLg.gap).toBe("$
|
|
380
|
+
expect(resolved.knobProps.gapLg.gap).toBe("$5");
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ── surface tiers ─────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
describe("surface tier recipes", () => {
|
|
387
|
+
it("cardSurface = bg + radius + panelPadding, no border/shadow", () => {
|
|
388
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
389
|
+
expect(knobProps.cardSurface).toEqual({
|
|
390
|
+
backgroundColor: "$color1",
|
|
391
|
+
borderRadius: "$4",
|
|
392
|
+
padding: "$4",
|
|
393
|
+
});
|
|
394
|
+
expect(knobProps.cardSurface).not.toHaveProperty("borderWidth");
|
|
395
|
+
expect(knobProps.cardSurface).not.toHaveProperty("elevation");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("cardSurface tracks borderRadius and space knobs (cap bites: large radius 16px > small padding 13px)", () => {
|
|
399
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "large", space: "small" }));
|
|
400
|
+
expect(knobProps.cardSurface.borderRadius).toBe(13);
|
|
401
|
+
expect(knobProps.cardSurface.padding).toBe("$3");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("cardSurface keeps the radius token while it fits the padding", () => {
|
|
405
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "large", space: "medium" }));
|
|
406
|
+
expect(knobProps.cardSurface.borderRadius).toBe("$6");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("elevatedSurface = surface + radius + elevation", () => {
|
|
410
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
411
|
+
expect(knobProps.elevatedSurface).toEqual({
|
|
412
|
+
...knobProps.surface,
|
|
413
|
+
borderRadius: knobProps.borderRadius.borderRadius,
|
|
414
|
+
elevation: knobProps.elevation,
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("featureSurface uses big radius, $6 padding, and an ultra-soft shadow", () => {
|
|
419
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
420
|
+
expect(knobProps.featureSurface.borderRadius).toBe("$10");
|
|
421
|
+
expect(knobProps.featureSurface.padding).toBe("$6");
|
|
422
|
+
expect(knobProps.featureSurface.shadowOffset).toEqual({ width: 0, height: 9 });
|
|
423
|
+
expect(knobProps.featureSurface.shadowRadius).toBe(12);
|
|
424
|
+
expect(knobProps.featureSurface).not.toHaveProperty("borderWidth");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ── container radius cap (Axiom 1 CLIP) ──────────────────
|
|
429
|
+
|
|
430
|
+
describe("containerRadius (Axiom 1 child-clip cap)", () => {
|
|
431
|
+
it("default knobs are untouched (radius $4 = 9px fits medium padding 18px)", () => {
|
|
432
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
433
|
+
expect(knobProps.containerRadius).toEqual({ borderRadius: "$4" });
|
|
434
|
+
expect(knobProps.cardSurface.borderRadius).toBe("$4");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("caps full radius at the padding px for every space", () => {
|
|
438
|
+
expect(
|
|
439
|
+
resolveKnobs(knobsWith({ borderRadius: "full", space: "small" })).knobProps.containerRadius
|
|
440
|
+
.borderRadius,
|
|
441
|
+
).toBe(13);
|
|
442
|
+
expect(
|
|
443
|
+
resolveKnobs(knobsWith({ borderRadius: "full", space: "medium" })).knobProps.containerRadius
|
|
444
|
+
.borderRadius,
|
|
445
|
+
).toBe(18);
|
|
446
|
+
expect(
|
|
447
|
+
resolveKnobs(knobsWith({ borderRadius: "full", space: "large" })).knobProps.containerRadius
|
|
448
|
+
.borderRadius,
|
|
449
|
+
).toBe(32);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("only bites at large/full: none/small/medium keep their token at every space", () => {
|
|
453
|
+
for (const space of Object.values(Space)) {
|
|
454
|
+
for (const [radius, token] of [
|
|
455
|
+
["none", "$0"],
|
|
456
|
+
["small", "$2"],
|
|
457
|
+
["medium", "$4"],
|
|
458
|
+
] as const) {
|
|
459
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: radius, space }));
|
|
460
|
+
expect(knobProps.containerRadius.borderRadius).toBe(token);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("large radius caps only under small space (16px > 13px)", () => {
|
|
466
|
+
expect(
|
|
467
|
+
resolveKnobs(knobsWith({ borderRadius: "large", space: "small" })).knobProps.containerRadius
|
|
468
|
+
.borderRadius,
|
|
469
|
+
).toBe(13);
|
|
470
|
+
expect(
|
|
471
|
+
resolveKnobs(knobsWith({ borderRadius: "large", space: "medium" })).knobProps
|
|
472
|
+
.containerRadius.borderRadius,
|
|
473
|
+
).toBe("$6");
|
|
474
|
+
expect(
|
|
475
|
+
resolveKnobs(knobsWith({ borderRadius: "large", space: "large" })).knobProps.containerRadius
|
|
476
|
+
.borderRadius,
|
|
477
|
+
).toBe("$6");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("mirrored px values agree with the live @tamagui/themes tokens (drift guard)", async () => {
|
|
481
|
+
const { tokens } = await import("@tamagui/themes");
|
|
482
|
+
const radiusTokenByKnob = {
|
|
483
|
+
none: "$0",
|
|
484
|
+
small: "$2",
|
|
485
|
+
medium: "$4",
|
|
486
|
+
large: "$6",
|
|
487
|
+
full: "$12",
|
|
488
|
+
} as const;
|
|
489
|
+
const paddingTokenByKnob = { small: "$3", medium: "$4", large: "$6" } as const;
|
|
490
|
+
for (const [radiusKnob, radiusToken] of Object.entries(radiusTokenByKnob)) {
|
|
491
|
+
for (const [spaceKnob, paddingToken] of Object.entries(paddingTokenByKnob)) {
|
|
492
|
+
const radiusPx = (tokens.radius as any)[radiusToken.slice(1)].val as number;
|
|
493
|
+
const paddingPx = (tokens.space as any)[paddingToken.slice(1)].val as number;
|
|
494
|
+
const expected = radiusPx > paddingPx ? paddingPx : radiusToken;
|
|
495
|
+
expect(
|
|
496
|
+
capContainerRadius(radiusKnob as Knobs["borderRadius"], spaceKnob as Knobs["space"]),
|
|
497
|
+
`radius=${radiusKnob} space=${spaceKnob}`,
|
|
498
|
+
).toBe(expected);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// ── cornerSmoothing (Axiom 15 OPTICS) ─────────────────────
|
|
505
|
+
|
|
506
|
+
describe("cornerSmoothing knob", () => {
|
|
507
|
+
const radiusFragments = [
|
|
508
|
+
"borderRadius",
|
|
509
|
+
"borderRadiusOuter",
|
|
510
|
+
"containerRadius",
|
|
511
|
+
"cardSurface",
|
|
512
|
+
"elevatedSurface",
|
|
513
|
+
"featureSurface",
|
|
514
|
+
] as const;
|
|
515
|
+
|
|
516
|
+
it('defaults to "round" and emits no class on any fragment', () => {
|
|
517
|
+
expect(defaultKnobs.cornerSmoothing).toBe(CornerSmoothing.Round);
|
|
518
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
519
|
+
for (const fragment of radiusFragments) {
|
|
520
|
+
expect(knobProps[fragment], fragment).not.toHaveProperty("className");
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('treats an absent knob as "round" (optional key, older presets)', () => {
|
|
525
|
+
const { cornerSmoothing: _omit, ...withoutKnob } = defaultKnobs;
|
|
526
|
+
const { knobProps } = resolveKnobs(withoutKnob as Knobs);
|
|
527
|
+
for (const fragment of radiusFragments) {
|
|
528
|
+
expect(knobProps[fragment], fragment).not.toHaveProperty("className");
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("smooth × arc classes (small/medium/large): every radius-bearing fragment carries the class", () => {
|
|
533
|
+
for (const radius of ["small", "medium", "large"] as const) {
|
|
534
|
+
const { knobProps } = resolveKnobs(
|
|
535
|
+
knobsWith({ cornerSmoothing: "smooth", borderRadius: radius }),
|
|
536
|
+
);
|
|
537
|
+
for (const fragment of radiusFragments) {
|
|
538
|
+
expect(knobProps[fragment].className, `${radius}/${fragment}`).toBe(
|
|
539
|
+
cornerSmoothClassName,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("smooth × none: nothing to smooth except the fixed-radius featureSurface", () => {
|
|
546
|
+
const { knobProps } = resolveKnobs(
|
|
547
|
+
knobsWith({ cornerSmoothing: "smooth", borderRadius: "none" }),
|
|
548
|
+
);
|
|
549
|
+
// $0 corners have no arc — square world stays square (Axiom 3 NULL).
|
|
550
|
+
expect(knobProps.borderRadius).not.toHaveProperty("className");
|
|
551
|
+
expect(knobProps.borderRadiusOuter).not.toHaveProperty("className");
|
|
552
|
+
expect(knobProps.containerRadius).not.toHaveProperty("className");
|
|
553
|
+
expect(knobProps.cardSurface).not.toHaveProperty("className");
|
|
554
|
+
expect(knobProps.elevatedSurface).not.toHaveProperty("className");
|
|
555
|
+
// featureSurface's radius is fixed ($10, knob-independent) — its arc
|
|
556
|
+
// exists at every knob value, so it smooths.
|
|
557
|
+
expect(knobProps.featureSurface.className).toBe(cornerSmoothClassName);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("smooth × full: pill/circle identity fragments ignore smoothing (R-BINARY/R-PILL)", () => {
|
|
561
|
+
const { knobProps } = resolveKnobs(
|
|
562
|
+
knobsWith({ cornerSmoothing: "smooth", borderRadius: "full" }),
|
|
563
|
+
);
|
|
564
|
+
// Uncapped $12 turns control-tier parts into capsules/circles —
|
|
565
|
+
// identity shapes that squircle ends would distort.
|
|
566
|
+
expect(knobProps.borderRadius).not.toHaveProperty("className");
|
|
567
|
+
expect(knobProps.elevatedSurface).not.toHaveProperty("className");
|
|
568
|
+
// Capped/outer fragments stay plain arcs (cap = padding px, outer ≤ $5)
|
|
569
|
+
// and keep smoothing.
|
|
570
|
+
expect(knobProps.borderRadiusOuter.className).toBe(cornerSmoothClassName);
|
|
571
|
+
expect(knobProps.containerRadius.className).toBe(cornerSmoothClassName);
|
|
572
|
+
expect(knobProps.cardSurface.className).toBe(cornerSmoothClassName);
|
|
573
|
+
expect(knobProps.featureSurface.className).toBe(cornerSmoothClassName);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("smooth keeps the Axiom 1 container cap (clamped radii keep their caps)", () => {
|
|
577
|
+
const smooth = resolveKnobs(
|
|
578
|
+
knobsWith({ cornerSmoothing: "smooth", borderRadius: "full", space: "small" }),
|
|
579
|
+
);
|
|
580
|
+
const round = resolveKnobs(knobsWith({ borderRadius: "full", space: "small" }));
|
|
581
|
+
expect(smooth.knobProps.containerRadius.borderRadius).toBe(13);
|
|
582
|
+
expect(round.knobProps.containerRadius.borderRadius).toBe(13);
|
|
583
|
+
expect(smooth.knobProps.cardSurface.borderRadius).toBe(13);
|
|
584
|
+
expect(smooth.knobProps.containerRadius.className).toBe(cornerSmoothClassName);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("smooth changes nothing but the class (radius/border/padding identical to round)", () => {
|
|
588
|
+
const round = resolveKnobs(knobsWith({ cornerSmoothing: "round" }));
|
|
589
|
+
const smooth = resolveKnobs(knobsWith({ cornerSmoothing: "smooth" }));
|
|
590
|
+
for (const fragment of radiusFragments) {
|
|
591
|
+
const { className: _cls, ...smoothRest } = smooth.knobProps[fragment];
|
|
592
|
+
expect(smoothRest, fragment).toEqual(round.knobProps[fragment]);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("never leaks onto non-radius fragments", () => {
|
|
597
|
+
const { knobProps } = resolveKnobs(knobsWith({ cornerSmoothing: "smooth" }));
|
|
598
|
+
expect(knobProps.surface).not.toHaveProperty("className");
|
|
599
|
+
expect(knobProps.inputSurface).not.toHaveProperty("className");
|
|
600
|
+
expect(knobProps.panelPadding).not.toHaveProperty("className");
|
|
601
|
+
expect(knobProps.gap).not.toHaveProperty("className");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("shipped presets pin the house default (round)", () => {
|
|
605
|
+
expect(defaultPreset.knobs.cornerSmoothing).toBe("round");
|
|
606
|
+
expect(boldPreset.knobs.cornerSmoothing).toBe("round");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Native resolves to `round` for now (SquircleView adoption is phase 2) —
|
|
610
|
+
// no className may reach RN views regardless of the knob.
|
|
611
|
+
it("native emits no class for either value", async () => {
|
|
612
|
+
vi.resetModules();
|
|
613
|
+
vi.doMock("@multiplatform.one/platform", () => ({ isWeb: false }));
|
|
614
|
+
const { resolveKnobs: resolveNative } = await import("./resolveKnobs");
|
|
615
|
+
try {
|
|
616
|
+
for (const cornerSmoothing of ["round", "smooth"] as const) {
|
|
617
|
+
const { knobProps } = resolveNative(knobsWith({ cornerSmoothing }));
|
|
618
|
+
for (const fragment of radiusFragments) {
|
|
619
|
+
expect(knobProps[fragment], `${cornerSmoothing}/${fragment}`).not.toHaveProperty(
|
|
620
|
+
"className",
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} finally {
|
|
625
|
+
vi.doUnmock("@multiplatform.one/platform");
|
|
626
|
+
vi.resetModules();
|
|
627
|
+
}
|
|
320
628
|
});
|
|
321
629
|
});
|
|
322
630
|
|
|
@@ -457,7 +765,7 @@ describe("resolveKnobs", () => {
|
|
|
457
765
|
const overridden = resolveKnobs(defaultKnobs, () => ({
|
|
458
766
|
elevation: "$4",
|
|
459
767
|
}));
|
|
460
|
-
expect(baseline.knobProps.elevation).
|
|
768
|
+
expect(baseline.knobProps.elevation).toBe("$1");
|
|
461
769
|
expect(overridden.knobProps.elevation).toBe("$4");
|
|
462
770
|
});
|
|
463
771
|
|
|
@@ -485,8 +793,8 @@ describe("resolveKnobs", () => {
|
|
|
485
793
|
|
|
486
794
|
const gapMap: Record<string, string> = {
|
|
487
795
|
small: "$2",
|
|
488
|
-
medium: "$
|
|
489
|
-
large: "$
|
|
796
|
+
medium: "$4",
|
|
797
|
+
large: "$5",
|
|
490
798
|
};
|
|
491
799
|
|
|
492
800
|
const panelPaddingMap: Record<string, string> = {
|
|
@@ -497,8 +805,8 @@ describe("resolveKnobs", () => {
|
|
|
497
805
|
|
|
498
806
|
const gapLgMap: Record<string, string> = {
|
|
499
807
|
small: "$3",
|
|
500
|
-
medium: "$
|
|
501
|
-
large: "$
|
|
808
|
+
medium: "$5",
|
|
809
|
+
large: "$6",
|
|
502
810
|
};
|
|
503
811
|
|
|
504
812
|
// T4.1a — every Size enum value produces a valid sizeToken
|
|
@@ -554,11 +862,11 @@ describe("resolveKnobs", () => {
|
|
|
554
862
|
expect(defaultKnobs.space).toBe("medium");
|
|
555
863
|
});
|
|
556
864
|
|
|
557
|
-
// T4.1i — emptyStateKnobs is still valid
|
|
865
|
+
// T4.1i — emptyStateKnobs is still valid (W12: default hover layer remains)
|
|
558
866
|
it("emptyStateKnobs is still valid (no required size field breaks it)", () => {
|
|
559
867
|
const knobs = knobsWith({ hover: emptyStateKnobs });
|
|
560
868
|
const { control } = resolveKnobs(knobs);
|
|
561
|
-
expect(control.hoverKnobProps).
|
|
869
|
+
expect(control.hoverKnobProps).toEqual({ backgroundColor: "$backgroundHover" });
|
|
562
870
|
});
|
|
563
871
|
|
|
564
872
|
// T4.1j — size "large" does not change borderRadius
|
|
@@ -569,3 +877,234 @@ describe("resolveKnobs", () => {
|
|
|
569
877
|
});
|
|
570
878
|
});
|
|
571
879
|
});
|
|
880
|
+
|
|
881
|
+
// ── Density (DG-DEN-01 / W8) ─────────────────────────────────
|
|
882
|
+
|
|
883
|
+
describe("applyDensity", () => {
|
|
884
|
+
it('defaultKnobs.density is "comfortable"', () => {
|
|
885
|
+
expect(defaultKnobs.density).toBe(Density.Comfortable);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it("comfortable density leaves size and space unchanged", () => {
|
|
889
|
+
const knobs = knobsWith({ size: "large", space: "large", density: "comfortable" });
|
|
890
|
+
const applied = applyDensity(knobs);
|
|
891
|
+
expect(applied.size).toBe("large");
|
|
892
|
+
expect(applied.space).toBe("large");
|
|
893
|
+
expect(applied.density).toBe("comfortable");
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it("compact density steps size and space down one level", () => {
|
|
897
|
+
expect(
|
|
898
|
+
applyDensity(knobsWith({ size: "large", space: "large", density: "compact" })),
|
|
899
|
+
).toMatchObject({ size: "medium", space: "medium", density: "compact" });
|
|
900
|
+
expect(
|
|
901
|
+
applyDensity(knobsWith({ size: "medium", space: "medium", density: "compact" })),
|
|
902
|
+
).toMatchObject({ size: "small", space: "small", density: "compact" });
|
|
903
|
+
expect(
|
|
904
|
+
applyDensity(knobsWith({ size: "small", space: "small", density: "compact" })),
|
|
905
|
+
).toMatchObject({ size: "small", space: "small", density: "compact" });
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it("compact:true override wins over comfortable density knob", () => {
|
|
909
|
+
const applied = applyDensity(
|
|
910
|
+
knobsWith({ size: "medium", space: "medium", density: "comfortable" }),
|
|
911
|
+
true,
|
|
912
|
+
);
|
|
913
|
+
expect(applied).toMatchObject({ size: "small", space: "small", density: "compact" });
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it("compact:false override wins over compact density knob", () => {
|
|
917
|
+
const applied = applyDensity(
|
|
918
|
+
knobsWith({ size: "medium", space: "medium", density: "compact" }),
|
|
919
|
+
false,
|
|
920
|
+
);
|
|
921
|
+
expect(applied).toMatchObject({ size: "medium", space: "medium", density: "comfortable" });
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it("resolveKnobs exposes density on knobProps", () => {
|
|
925
|
+
expect(resolveKnobs(defaultKnobs).knobProps.density).toBe("comfortable");
|
|
926
|
+
expect(resolveKnobs(knobsWith({ density: "compact" })).knobProps.density).toBe("compact");
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it("compact density produces smaller sizeToken and gap than comfortable", () => {
|
|
930
|
+
const comfortable = resolveKnobs(applyDensity(knobsWith({ size: "medium", space: "medium" })));
|
|
931
|
+
const compact = resolveKnobs(
|
|
932
|
+
applyDensity(knobsWith({ size: "medium", space: "medium", density: "compact" })),
|
|
933
|
+
);
|
|
934
|
+
expect(comfortable.knobProps.sizeToken).toBe("$4");
|
|
935
|
+
expect(compact.knobProps.sizeToken).toBe("$3");
|
|
936
|
+
expect(comfortable.knobProps.gap).toEqual({ gap: "$4" });
|
|
937
|
+
expect(compact.knobProps.gap).toEqual({ gap: "$2" });
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it("compact density preserves house-decision knobs", () => {
|
|
941
|
+
const knobs = knobsWith({
|
|
942
|
+
density: "compact",
|
|
943
|
+
fieldLabelPlacement: "side",
|
|
944
|
+
requiredMarking: "asterisk",
|
|
945
|
+
tableZebra: "on",
|
|
946
|
+
bulkBarPlacement: "bottom",
|
|
947
|
+
selectAllScope: "filtered",
|
|
948
|
+
timestampStyle: "relative",
|
|
949
|
+
disabledStyle: "dimWhole",
|
|
950
|
+
formAutofocus: "on",
|
|
951
|
+
});
|
|
952
|
+
const applied = applyDensity(knobs);
|
|
953
|
+
expect(applied).toMatchObject({
|
|
954
|
+
density: "compact",
|
|
955
|
+
fieldLabelPlacement: "side",
|
|
956
|
+
requiredMarking: "asterisk",
|
|
957
|
+
tableZebra: "on",
|
|
958
|
+
bulkBarPlacement: "bottom",
|
|
959
|
+
selectAllScope: "filtered",
|
|
960
|
+
timestampStyle: "relative",
|
|
961
|
+
disabledStyle: "dimWhole",
|
|
962
|
+
formAutofocus: "on",
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// ── House decisions (design-guidelines.md) ────────────────────
|
|
968
|
+
|
|
969
|
+
describe("house decision knobs", () => {
|
|
970
|
+
it("defaultKnobs use recommended house defaults", () => {
|
|
971
|
+
expect(defaultKnobs.fieldLabelPlacement).toBe(FieldLabelPlacement.Top);
|
|
972
|
+
expect(defaultKnobs.requiredMarking).toBe(RequiredMarking.Minority);
|
|
973
|
+
expect(defaultKnobs.tableZebra).toBe(TableZebra.Off);
|
|
974
|
+
expect(defaultKnobs.bulkBarPlacement).toBe(BulkBarPlacement.Top);
|
|
975
|
+
expect(defaultKnobs.selectAllScope).toBe(SelectAllScope.Page);
|
|
976
|
+
expect(defaultKnobs.timestampStyle).toBe(TimestampStyle.Absolute);
|
|
977
|
+
expect(defaultKnobs.disabledStyle).toBe(DisabledStyle.KeepLabel);
|
|
978
|
+
expect(defaultKnobs.formAutofocus).toBe(FormAutofocus.Off);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it("resolveKnobs exposes house knobs on knobProps (pass-through)", () => {
|
|
982
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
983
|
+
expect(knobProps.fieldLabelPlacement).toBe("top");
|
|
984
|
+
expect(knobProps.requiredMarking).toBe("minority");
|
|
985
|
+
expect(knobProps.tableZebra).toBe("off");
|
|
986
|
+
expect(knobProps.bulkBarPlacement).toBe("top");
|
|
987
|
+
expect(knobProps.selectAllScope).toBe("page");
|
|
988
|
+
expect(knobProps.timestampStyle).toBe("absolute");
|
|
989
|
+
expect(knobProps.disabledStyle).toBe("keepLabel");
|
|
990
|
+
expect(knobProps.formAutofocus).toBe("off");
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it("resolveKnobs reflects house knob overrides", () => {
|
|
994
|
+
const { knobProps } = resolveKnobs(
|
|
995
|
+
knobsWith({
|
|
996
|
+
fieldLabelPlacement: "floating",
|
|
997
|
+
requiredMarking: "optional",
|
|
998
|
+
tableZebra: "on",
|
|
999
|
+
bulkBarPlacement: "bottom",
|
|
1000
|
+
selectAllScope: "filtered",
|
|
1001
|
+
timestampStyle: "relative",
|
|
1002
|
+
disabledStyle: "dimWhole",
|
|
1003
|
+
formAutofocus: "on",
|
|
1004
|
+
}),
|
|
1005
|
+
);
|
|
1006
|
+
expect(knobProps.fieldLabelPlacement).toBe("floating");
|
|
1007
|
+
expect(knobProps.requiredMarking).toBe("optional");
|
|
1008
|
+
expect(knobProps.tableZebra).toBe("on");
|
|
1009
|
+
expect(knobProps.bulkBarPlacement).toBe("bottom");
|
|
1010
|
+
expect(knobProps.selectAllScope).toBe("filtered");
|
|
1011
|
+
expect(knobProps.timestampStyle).toBe("relative");
|
|
1012
|
+
expect(knobProps.disabledStyle).toBe("dimWhole");
|
|
1013
|
+
expect(knobProps.formAutofocus).toBe("on");
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it("house knobs do not alter structural token resolution", () => {
|
|
1017
|
+
const baseline = resolveKnobs(defaultKnobs);
|
|
1018
|
+
const housey = resolveKnobs(
|
|
1019
|
+
knobsWith({
|
|
1020
|
+
fieldLabelPlacement: "side",
|
|
1021
|
+
tableZebra: "on",
|
|
1022
|
+
formAutofocus: "on",
|
|
1023
|
+
}),
|
|
1024
|
+
);
|
|
1025
|
+
expect(housey.knobProps.sizeToken).toBe(baseline.knobProps.sizeToken);
|
|
1026
|
+
expect(housey.knobProps.gap).toEqual(baseline.knobProps.gap);
|
|
1027
|
+
expect(housey.knobProps.borderRadius).toEqual(baseline.knobProps.borderRadius);
|
|
1028
|
+
expect(housey.knobProps.density).toBe(baseline.knobProps.density);
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// ── Disabled treatment recipe (LC-40 DISABLED-VISIBLE) ────────
|
|
1033
|
+
|
|
1034
|
+
describe("disabledState recipe (LC-40)", () => {
|
|
1035
|
+
it("keepLabel (default) resolves a VISIBLE chrome treatment — not a no-op", () => {
|
|
1036
|
+
const { disabledState } = resolveKnobs(defaultKnobs);
|
|
1037
|
+
expect(disabledState.style).toBe("keepLabel");
|
|
1038
|
+
// Text-bearing chrome: fill/border wash to muted ramp steps, no opacity
|
|
1039
|
+
// (inner text keeps its own alpha so it can hold >= AA).
|
|
1040
|
+
expect(disabledState.surfaceKnobProps).toEqual({
|
|
1041
|
+
backgroundColor: "$color3",
|
|
1042
|
+
borderColor: "$color6",
|
|
1043
|
+
cursor: "not-allowed",
|
|
1044
|
+
});
|
|
1045
|
+
expect(disabledState.surfaceKnobProps).not.toHaveProperty("opacity");
|
|
1046
|
+
// Selected-while-disabled tier: one wash step stronger than the plain
|
|
1047
|
+
// wash so the chosen segment stays visibly chosen (Axiom 6).
|
|
1048
|
+
expect(disabledState.selectedSurfaceKnobProps).toEqual({
|
|
1049
|
+
backgroundColor: "$color5",
|
|
1050
|
+
borderColor: "$color8",
|
|
1051
|
+
cursor: "not-allowed",
|
|
1052
|
+
});
|
|
1053
|
+
// Text-free chrome: opacity dim.
|
|
1054
|
+
expect(disabledState.chromeKnobProps).toEqual({ opacity: 0.5, cursor: "not-allowed" });
|
|
1055
|
+
// Inner text re-anchors to the documented lowest AA step ($color11).
|
|
1056
|
+
expect(disabledState.textKnobProps).toEqual({ color: "$color11" });
|
|
1057
|
+
// keepLabel never dims the assembly (label/helper stay fully readable).
|
|
1058
|
+
expect(disabledState.assemblyKnobProps).toEqual({});
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it("dimWhole dims the assembly once — control fragments carry only the affordance", () => {
|
|
1062
|
+
const { disabledState } = resolveKnobs(knobsWith({ disabledStyle: "dimWhole" }));
|
|
1063
|
+
expect(disabledState.style).toBe("dimWhole");
|
|
1064
|
+
expect(disabledState.assemblyKnobProps).toEqual({ opacity: 0.5 });
|
|
1065
|
+
// No chrome-level opacity: the assembly owner applies the single dim.
|
|
1066
|
+
expect(disabledState.surfaceKnobProps).toEqual({ cursor: "not-allowed" });
|
|
1067
|
+
expect(disabledState.chromeKnobProps).toEqual({ cursor: "not-allowed" });
|
|
1068
|
+
expect(disabledState.textKnobProps).toEqual({});
|
|
1069
|
+
// Exception: the selected tier still washes — consumers drop the live
|
|
1070
|
+
// active fill when disabled, so the assembly dim alone cannot show
|
|
1071
|
+
// which option is chosen.
|
|
1072
|
+
expect(disabledState.selectedSurfaceKnobProps).toEqual({
|
|
1073
|
+
backgroundColor: "$color5",
|
|
1074
|
+
borderColor: "$color8",
|
|
1075
|
+
cursor: "not-allowed",
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it("both values expose complete fragments for every anatomy slot", () => {
|
|
1080
|
+
for (const disabledStyle of ["keepLabel", "dimWhole"] as const) {
|
|
1081
|
+
const { disabledState } = resolveKnobs(knobsWith({ disabledStyle }));
|
|
1082
|
+
expect(disabledState).toHaveProperty("surfaceKnobProps");
|
|
1083
|
+
expect(disabledState).toHaveProperty("selectedSurfaceKnobProps");
|
|
1084
|
+
expect(disabledState).toHaveProperty("chromeKnobProps");
|
|
1085
|
+
expect(disabledState).toHaveProperty("textKnobProps");
|
|
1086
|
+
expect(disabledState).toHaveProperty("assemblyKnobProps");
|
|
1087
|
+
// The not-allowed affordance rides the control chrome under both values.
|
|
1088
|
+
expect(disabledState.surfaceKnobProps.cursor).toBe("not-allowed");
|
|
1089
|
+
expect(disabledState.selectedSurfaceKnobProps.cursor).toBe("not-allowed");
|
|
1090
|
+
expect(disabledState.chromeKnobProps.cursor).toBe("not-allowed");
|
|
1091
|
+
// Selected tier is visibly distinct from the plain wash in both models.
|
|
1092
|
+
expect(disabledState.selectedSurfaceKnobProps.backgroundColor).not.toBe(
|
|
1093
|
+
disabledState.surfaceKnobProps.backgroundColor,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it("disabled tiers are theme-relative ramp steps (no hex, works under intents)", () => {
|
|
1099
|
+
const { disabledState } = resolveKnobs(defaultKnobs);
|
|
1100
|
+
for (const value of [
|
|
1101
|
+
disabledState.surfaceKnobProps.backgroundColor,
|
|
1102
|
+
disabledState.surfaceKnobProps.borderColor,
|
|
1103
|
+
disabledState.selectedSurfaceKnobProps.backgroundColor,
|
|
1104
|
+
disabledState.selectedSurfaceKnobProps.borderColor,
|
|
1105
|
+
disabledState.textKnobProps.color,
|
|
1106
|
+
]) {
|
|
1107
|
+
expect(value).toMatch(/^\$color\d+$/);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
});
|