@multiplatform.one/theme 6.1.0 → 6.3.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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPTICS hairline (Axiom 15): a divider means "one device pixel", not "one
|
|
3
|
+
* CSS pixel". On high-DPI web screens a 1px CSS divider paints 2–3 device
|
|
4
|
+
* pixels and reads heavy; 0.5px paints a single device pixel on 2x displays.
|
|
5
|
+
*
|
|
6
|
+
* Engine notes (probed against Chromium 147 / WebKit / Gecko):
|
|
7
|
+
* - Blink floors sub-1px BORDER widths up to 1 CSS px, but honors fractional
|
|
8
|
+
* HEIGHT/WIDTH — so standalone rules use the filled `line`/`vline`
|
|
9
|
+
* treatments (crisp everywhere), while container-owned edge borders
|
|
10
|
+
* (`bottom`/`top`/`right`/`left`) render true hairlines on WebKit/Gecko
|
|
11
|
+
* and gracefully stay 1px on Blink.
|
|
12
|
+
* - Low-DPI screens (1dppx) get a 1px fallback via the resolution media
|
|
13
|
+
* query below, so hairlines never fade or vanish there.
|
|
14
|
+
*
|
|
15
|
+
* Scope: DIVIDERS ONLY — separators between rows/sections, menu separators,
|
|
16
|
+
* table head rules, full-bleed panel rules. Control borders (inputs,
|
|
17
|
+
* buttons, control groups) keep the knob-driven `borderWidth` semantics
|
|
18
|
+
* from `resolveKnobs` and MUST NOT use this token.
|
|
19
|
+
*
|
|
20
|
+
* Native twin: `hairline.native.ts` (react-native StyleSheet.hairlineWidth,
|
|
21
|
+
* no CSS classes).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Divider line width: half a CSS pixel = one device pixel on 2x displays. */
|
|
25
|
+
export const hairlineWidth: number = 0.5;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Spreadable divider treatments. Each entry pairs the 0.5px style with the
|
|
29
|
+
* fallback class the low-DPI media query targets:
|
|
30
|
+
*
|
|
31
|
+
* <Separator {...hairline.line} /> // horizontal rule element
|
|
32
|
+
* <Separator vertical {...hairline.vline} /> // vertical rule element
|
|
33
|
+
* <View {...hairline.height} /> // already-filled 1px line
|
|
34
|
+
* <YStack {...hairline.bottom} /> // container-owned edge rule
|
|
35
|
+
*/
|
|
36
|
+
export const hairline = {
|
|
37
|
+
/**
|
|
38
|
+
* Standalone horizontal rule as a FILLED line (tamagui Separator draws a
|
|
39
|
+
* border, which Blink cannot paint below 1 CSS px — flip it to a filled
|
|
40
|
+
* fractional-height line, crisp in every engine). Consumers may override
|
|
41
|
+
* `backgroundColor` after the spread for non-default divider colors.
|
|
42
|
+
*/
|
|
43
|
+
line: {
|
|
44
|
+
height: hairlineWidth,
|
|
45
|
+
maxHeight: hairlineWidth,
|
|
46
|
+
// Separator is a `flex: 1` item, so in a stack the main-axis size comes
|
|
47
|
+
// from flex-basis (height alone is ignored):
|
|
48
|
+
flexBasis: hairlineWidth,
|
|
49
|
+
borderBottomWidth: 0,
|
|
50
|
+
backgroundColor: "$borderColor",
|
|
51
|
+
className: "mp-hairline-h",
|
|
52
|
+
},
|
|
53
|
+
/** Standalone vertical rule as a filled line (see `line`). */
|
|
54
|
+
vline: {
|
|
55
|
+
width: hairlineWidth,
|
|
56
|
+
maxWidth: hairlineWidth,
|
|
57
|
+
flexBasis: hairlineWidth,
|
|
58
|
+
borderRightWidth: 0,
|
|
59
|
+
backgroundColor: "$borderColor",
|
|
60
|
+
className: "mp-hairline-w",
|
|
61
|
+
},
|
|
62
|
+
/** Already-filled 1px line elements (e.g. menu separator views). */
|
|
63
|
+
height: { height: hairlineWidth, className: "mp-hairline-h" },
|
|
64
|
+
// Container-owned edge rules. True hairline on WebKit/Gecko; Blink rounds
|
|
65
|
+
// sub-1px borders up to 1 CSS px (the pre-hairline status quo there).
|
|
66
|
+
bottom: { borderBottomWidth: hairlineWidth, className: "mp-hairline-b" },
|
|
67
|
+
top: { borderTopWidth: hairlineWidth, className: "mp-hairline-t" },
|
|
68
|
+
right: { borderRightWidth: hairlineWidth, className: "mp-hairline-r" },
|
|
69
|
+
left: { borderLeftWidth: hairlineWidth, className: "mp-hairline-l" },
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
const STYLE_TAG_ID = "mp-hairline-styles";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 1px fallback for screens where 0.5px cannot map to a whole device pixel.
|
|
76
|
+
* (`-webkit-max-device-pixel-ratio` covers Safari versions without
|
|
77
|
+
* `resolution` media-query support.) Exported for unit tests.
|
|
78
|
+
*/
|
|
79
|
+
export const hairlineFallbackCss = `@media (max-resolution: 1.49dppx), (-webkit-max-device-pixel-ratio: 1.49) {
|
|
80
|
+
.mp-hairline-b { border-bottom-width: 1px !important; }
|
|
81
|
+
.mp-hairline-t { border-top-width: 1px !important; }
|
|
82
|
+
.mp-hairline-r { border-right-width: 1px !important; }
|
|
83
|
+
.mp-hairline-l { border-left-width: 1px !important; }
|
|
84
|
+
.mp-hairline-h { height: 1px !important; max-height: 1px !important; flex-basis: 1px !important; }
|
|
85
|
+
.mp-hairline-w { width: 1px !important; max-width: 1px !important; flex-basis: 1px !important; }
|
|
86
|
+
.mp-hairline-ie { border-inline-end-width: 1px !important; }
|
|
87
|
+
}`;
|
|
88
|
+
|
|
89
|
+
/** Idempotently mount the low-DPI fallback stylesheet (no-op off-DOM). */
|
|
90
|
+
export function ensureHairlineFallback(): void {
|
|
91
|
+
if (typeof document === "undefined") return;
|
|
92
|
+
if (document.getElementById(STYLE_TAG_ID)) return;
|
|
93
|
+
const tag = document.createElement("style");
|
|
94
|
+
tag.id = STYLE_TAG_ID;
|
|
95
|
+
tag.textContent = hairlineFallbackCss;
|
|
96
|
+
document.head.appendChild(tag);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The fallback is static CSS (no knob/theme dependency), so mount it as soon
|
|
100
|
+
// as the module loads in a DOM environment.
|
|
101
|
+
ensureHairlineFallback();
|
package/src/theme/index.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
export * from "./animations/index";
|
|
2
|
+
export * from "./chartPalette";
|
|
2
3
|
export * from "./colorRules";
|
|
3
4
|
export * from "./cookies";
|
|
5
|
+
export * from "./cornerSmoothing";
|
|
4
6
|
export * from "./createDefaultThemeConfig";
|
|
5
7
|
export * from "./createThemes";
|
|
8
|
+
export * from "./defaults/index";
|
|
6
9
|
export * from "./devtools/ThemeDevtoolsPanel";
|
|
10
|
+
export * from "./focusState";
|
|
11
|
+
export * from "./FontKnobStyles";
|
|
12
|
+
export * from "./hairline";
|
|
7
13
|
export * from "./Intent";
|
|
8
14
|
export * from "./intents";
|
|
9
15
|
export * from "./knobs";
|
|
16
|
+
export * from "./layoutTokens";
|
|
10
17
|
export * from "./Preset";
|
|
11
18
|
export type { Preset as PresetDefinition, IntentOverride } from "./preset.types";
|
|
12
19
|
export * from "./PresetContext";
|
|
13
20
|
export * from "./presets";
|
|
21
|
+
export * from "./readableColor";
|
|
14
22
|
export * from "./recipes";
|
|
15
23
|
export * from "./resolveKnobs";
|
|
16
24
|
export * from "./shared";
|
|
@@ -269,8 +269,8 @@ describe("useResolvedKnobs with Intent", () => {
|
|
|
269
269
|
|
|
270
270
|
renderToHtml(wrapWithPreset(makePreset(), createElement(capture.Probe)));
|
|
271
271
|
|
|
272
|
-
// Default knobs: elevation "
|
|
273
|
-
expect(capture.result.knobProps.elevation).
|
|
272
|
+
// Default knobs: elevation "small" → resolved to "$1"
|
|
273
|
+
expect(capture.result.knobProps.elevation).toBe("$1");
|
|
274
274
|
expect(capture.result.knobProps.textAccent).toBe("high");
|
|
275
275
|
});
|
|
276
276
|
});
|
package/src/theme/intents.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { IntentOverride } from "./preset.types";
|
|
2
2
|
|
|
3
|
+
// One emphasis system (SB-E-01): every intent Button renders SOLID like
|
|
4
|
+
// accent — fillStyle "filled" so the solid _Button sub-theme surface applies
|
|
5
|
+
// even under outlined presets.
|
|
3
6
|
export const defaultIntents: Record<string, IntentOverride> = {
|
|
4
7
|
accent: {
|
|
5
8
|
fillStyle: "outlined",
|
|
@@ -8,15 +11,16 @@ export const defaultIntents: Record<string, IntentOverride> = {
|
|
|
8
11
|
},
|
|
9
12
|
error: {
|
|
10
13
|
textAccent: "high",
|
|
11
|
-
Button: { elevation: "small" },
|
|
14
|
+
Button: { fillStyle: "filled", elevation: "small" },
|
|
12
15
|
Input: { borderWidth: "medium" },
|
|
13
16
|
},
|
|
14
17
|
warning: {
|
|
15
18
|
textAccent: "high",
|
|
19
|
+
Button: { fillStyle: "filled" },
|
|
16
20
|
Input: { borderWidth: "small" },
|
|
17
21
|
},
|
|
18
22
|
success: {
|
|
19
23
|
textAccent: "high",
|
|
20
|
-
Button: { elevation: "none" },
|
|
24
|
+
Button: { fillStyle: "filled", elevation: "none" },
|
|
21
25
|
},
|
|
22
26
|
};
|
package/src/theme/knobs.ts
CHANGED
|
@@ -12,6 +12,22 @@ export const BorderRadius = {
|
|
|
12
12
|
} as const;
|
|
13
13
|
export type BorderRadius = (typeof BorderRadius)[keyof typeof BorderRadius];
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Corner curvature continuity (Axiom 15 OPTICS). `smooth` renders
|
|
17
|
+
* continuous-curvature (superellipse/squircle) corners where the platform
|
|
18
|
+
* can (web: Chromium 139+ via `corner-shape`, others fall back to the plain
|
|
19
|
+
* arc); `round` is today's circular-arc border-radius everywhere.
|
|
20
|
+
* Two values, not a numeric dial: every shipped smoothing system converges
|
|
21
|
+
* on one blessed curve (Apple ≈ Figma 0.6 ≈ superellipse K=2), and a free
|
|
22
|
+
* number invites per-screen drift (Axiom 5). `none` is not needed —
|
|
23
|
+
* `borderRadius: "none"` already covers square corners.
|
|
24
|
+
*/
|
|
25
|
+
export const CornerSmoothing = {
|
|
26
|
+
Round: "round",
|
|
27
|
+
Smooth: "smooth",
|
|
28
|
+
} as const;
|
|
29
|
+
export type CornerSmoothing = (typeof CornerSmoothing)[keyof typeof CornerSmoothing];
|
|
30
|
+
|
|
15
31
|
export const BorderWidth = {
|
|
16
32
|
None: "none",
|
|
17
33
|
Small: "small",
|
|
@@ -42,6 +58,89 @@ export const Size = {
|
|
|
42
58
|
} as const;
|
|
43
59
|
export type Size = (typeof Size)[keyof typeof Size];
|
|
44
60
|
|
|
61
|
+
/** Global density mode — compact steps size+space down one level. */
|
|
62
|
+
export const Density = {
|
|
63
|
+
Comfortable: "comfortable",
|
|
64
|
+
Compact: "compact",
|
|
65
|
+
} as const;
|
|
66
|
+
export type Density = (typeof Density)[keyof typeof Density];
|
|
67
|
+
|
|
68
|
+
// ── House decisions (design-guidelines.md) ─────────────────
|
|
69
|
+
// Behavioral knobs with recommended defaults — not hard constraints.
|
|
70
|
+
|
|
71
|
+
/** Field label placement relative to the control. Default: top. */
|
|
72
|
+
export const FieldLabelPlacement = {
|
|
73
|
+
Top: "top",
|
|
74
|
+
Side: "side",
|
|
75
|
+
Floating: "floating",
|
|
76
|
+
} as const;
|
|
77
|
+
export type FieldLabelPlacement = (typeof FieldLabelPlacement)[keyof typeof FieldLabelPlacement];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How required/optional fields are marked.
|
|
81
|
+
* `minority` marks whichever is less common in the form (compute from schema).
|
|
82
|
+
*/
|
|
83
|
+
export const RequiredMarking = {
|
|
84
|
+
Asterisk: "asterisk",
|
|
85
|
+
Optional: "optional",
|
|
86
|
+
Minority: "minority",
|
|
87
|
+
} as const;
|
|
88
|
+
export type RequiredMarking = (typeof RequiredMarking)[keyof typeof RequiredMarking];
|
|
89
|
+
|
|
90
|
+
/** Alternating table row backgrounds. Default: off. */
|
|
91
|
+
export const TableZebra = {
|
|
92
|
+
On: "on",
|
|
93
|
+
Off: "off",
|
|
94
|
+
} as const;
|
|
95
|
+
export type TableZebra = (typeof TableZebra)[keyof typeof TableZebra];
|
|
96
|
+
|
|
97
|
+
/** Bulk-action bar placement for multi-select tables. Default: top (sticky). */
|
|
98
|
+
export const BulkBarPlacement = {
|
|
99
|
+
Top: "top",
|
|
100
|
+
Bottom: "bottom",
|
|
101
|
+
} as const;
|
|
102
|
+
export type BulkBarPlacement = (typeof BulkBarPlacement)[keyof typeof BulkBarPlacement];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Timestamp display style.
|
|
106
|
+
* Recommended: absolute in data tables; relative in feeds. Default: absolute.
|
|
107
|
+
*/
|
|
108
|
+
export const TimestampStyle = {
|
|
109
|
+
Absolute: "absolute",
|
|
110
|
+
Relative: "relative",
|
|
111
|
+
} as const;
|
|
112
|
+
export type TimestampStyle = (typeof TimestampStyle)[keyof typeof TimestampStyle];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Table select-all scope (DG-TBL-03): header checkbox selects the current
|
|
116
|
+
* page or all filtered/matching rows. Default: page.
|
|
117
|
+
*/
|
|
118
|
+
export const SelectAllScope = {
|
|
119
|
+
Page: "page",
|
|
120
|
+
Filtered: "filtered",
|
|
121
|
+
} as const;
|
|
122
|
+
export type SelectAllScope = (typeof SelectAllScope)[keyof typeof SelectAllScope];
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Disabled control treatment (LC-40 DISABLED-VISIBLE / DG-ST-05). Both
|
|
126
|
+
* values render a VISIBLE treatment via the resolved `disabledState` recipe:
|
|
127
|
+
* `keepLabel` (default, Fluent model) washes the control chrome while
|
|
128
|
+
* label/value text stays ≥ AA; `dimWhole` (Material model) dims the whole
|
|
129
|
+
* assembly, label included. Read-only is a different state and never dims.
|
|
130
|
+
*/
|
|
131
|
+
export const DisabledStyle = {
|
|
132
|
+
DimWhole: "dimWhole",
|
|
133
|
+
KeepLabel: "keepLabel",
|
|
134
|
+
} as const;
|
|
135
|
+
export type DisabledStyle = (typeof DisabledStyle)[keyof typeof DisabledStyle];
|
|
136
|
+
|
|
137
|
+
/** Autofocus first field on form mount. Default: off (a11y). */
|
|
138
|
+
export const FormAutofocus = {
|
|
139
|
+
On: "on",
|
|
140
|
+
Off: "off",
|
|
141
|
+
} as const;
|
|
142
|
+
export type FormAutofocus = (typeof FormAutofocus)[keyof typeof FormAutofocus];
|
|
143
|
+
|
|
45
144
|
// ── Emphasis ───────────────────────────────────────────────
|
|
46
145
|
|
|
47
146
|
export const TextAccent = {
|
|
@@ -109,15 +208,38 @@ export type InteractionState = (typeof interactionStates)[number];
|
|
|
109
208
|
export interface Knobs {
|
|
110
209
|
fillStyle: FillStyle;
|
|
111
210
|
borderRadius: BorderRadius;
|
|
211
|
+
/**
|
|
212
|
+
* Corner curvature continuity (Axiom 15 OPTICS). Optional so existing
|
|
213
|
+
* preset/knob constructions stay valid; resolveKnobs defaults to `round`.
|
|
214
|
+
*/
|
|
215
|
+
cornerSmoothing?: CornerSmoothing;
|
|
112
216
|
borderWidth: BorderWidth;
|
|
113
217
|
elevation: Elevation;
|
|
114
218
|
space: Space;
|
|
115
219
|
size: Size;
|
|
220
|
+
/** Propagating density mode. Default comfortable; compact steps size+space. */
|
|
221
|
+
density: Density;
|
|
116
222
|
textAccent: TextAccent;
|
|
117
223
|
headingFont: HeadingFont;
|
|
118
224
|
bodyFont: BodyFont;
|
|
119
225
|
fontWeight: FontWeight;
|
|
120
226
|
animation: Animation;
|
|
227
|
+
/** House: field label placement. Default top. */
|
|
228
|
+
fieldLabelPlacement: FieldLabelPlacement;
|
|
229
|
+
/** House: required/optional marking. Default minority. */
|
|
230
|
+
requiredMarking: RequiredMarking;
|
|
231
|
+
/** House: zebra table rows. Default off. */
|
|
232
|
+
tableZebra: TableZebra;
|
|
233
|
+
/** House: bulk bar placement. Default top. */
|
|
234
|
+
bulkBarPlacement: BulkBarPlacement;
|
|
235
|
+
/** House: table select-all scope. Default page. */
|
|
236
|
+
selectAllScope: SelectAllScope;
|
|
237
|
+
/** House: timestamp style. Default absolute. */
|
|
238
|
+
timestampStyle: TimestampStyle;
|
|
239
|
+
/** House: disabled treatment. Default keepLabel. */
|
|
240
|
+
disabledStyle: DisabledStyle;
|
|
241
|
+
/** House: form autofocus. Default off. */
|
|
242
|
+
formAutofocus: FormAutofocus;
|
|
121
243
|
hover: StateKnobs;
|
|
122
244
|
press: StateKnobs;
|
|
123
245
|
focus: StateKnobs;
|
|
@@ -131,15 +253,25 @@ export const emptyStateKnobs: StateKnobs = {};
|
|
|
131
253
|
export const defaultKnobs: Knobs = {
|
|
132
254
|
fillStyle: "filled",
|
|
133
255
|
borderRadius: "medium",
|
|
134
|
-
|
|
135
|
-
|
|
256
|
+
cornerSmoothing: "round",
|
|
257
|
+
borderWidth: "medium",
|
|
258
|
+
elevation: "small",
|
|
136
259
|
space: "medium",
|
|
137
260
|
size: "medium",
|
|
261
|
+
density: "comfortable",
|
|
138
262
|
textAccent: "high",
|
|
139
263
|
headingFont: "sans-serif",
|
|
140
264
|
bodyFont: "sans-serif",
|
|
141
265
|
fontWeight: "regular",
|
|
142
266
|
animation: "quick",
|
|
267
|
+
fieldLabelPlacement: "top",
|
|
268
|
+
requiredMarking: "minority",
|
|
269
|
+
tableZebra: "off",
|
|
270
|
+
bulkBarPlacement: "top",
|
|
271
|
+
selectAllScope: "page",
|
|
272
|
+
timestampStyle: "absolute",
|
|
273
|
+
disabledStyle: "keepLabel",
|
|
274
|
+
formAutofocus: "off",
|
|
143
275
|
hover: emptyStateKnobs,
|
|
144
276
|
press: emptyStateKnobs,
|
|
145
277
|
focus: emptyStateKnobs,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
MIN_PRESS_TARGET,
|
|
4
|
+
NARROW_BREAKPOINT,
|
|
5
|
+
OVERLAY_BREAKPOINT,
|
|
6
|
+
READING_WIDTH_CH,
|
|
7
|
+
READING_WIDTH_MAX_CH,
|
|
8
|
+
READING_WIDTH_MIN_CH,
|
|
9
|
+
defaultMaxPanes,
|
|
10
|
+
getLayoutSizeClass,
|
|
11
|
+
layoutBreakpoints,
|
|
12
|
+
layoutSizeClassAtLeast,
|
|
13
|
+
pressTargetHitSlop,
|
|
14
|
+
pressTargetStyle,
|
|
15
|
+
readingWidthStyle,
|
|
16
|
+
} from "./layoutTokens";
|
|
17
|
+
|
|
18
|
+
describe("layoutBreakpoints (DG-LAY-01)", () => {
|
|
19
|
+
it("documents FloatingPanel 860 as expanded / overlay", () => {
|
|
20
|
+
expect(layoutBreakpoints.expanded).toBe(860);
|
|
21
|
+
expect(OVERLAY_BREAKPOINT).toBe(860);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("documents DeskShell narrow as medium 640", () => {
|
|
25
|
+
expect(layoutBreakpoints.medium).toBe(640);
|
|
26
|
+
expect(NARROW_BREAKPOINT).toBe(640);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("maps widths to size classes", () => {
|
|
30
|
+
expect(getLayoutSizeClass(360)).toBe("compact");
|
|
31
|
+
expect(getLayoutSizeClass(640)).toBe("medium");
|
|
32
|
+
expect(getLayoutSizeClass(859)).toBe("medium");
|
|
33
|
+
expect(getLayoutSizeClass(860)).toBe("expanded");
|
|
34
|
+
expect(getLayoutSizeClass(1024)).toBe("large");
|
|
35
|
+
expect(getLayoutSizeClass(1280)).toBe("xl");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("defaults to 1 pane below expanded, 2 at expanded+", () => {
|
|
39
|
+
expect(defaultMaxPanes("compact")).toBe(1);
|
|
40
|
+
expect(defaultMaxPanes("medium")).toBe(1);
|
|
41
|
+
expect(defaultMaxPanes("expanded")).toBe(2);
|
|
42
|
+
expect(defaultMaxPanes("large")).toBe(2);
|
|
43
|
+
expect(defaultMaxPanes("xl")).toBe(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("compares size classes ordinally", () => {
|
|
47
|
+
expect(layoutSizeClassAtLeast("expanded", "expanded")).toBe(true);
|
|
48
|
+
expect(layoutSizeClassAtLeast("medium", "expanded")).toBe(false);
|
|
49
|
+
expect(layoutSizeClassAtLeast("xl", "large")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("readingWidthStyle (DG-LAY-05)", () => {
|
|
54
|
+
it("defaults to ~70ch within 65–75", () => {
|
|
55
|
+
expect(READING_WIDTH_CH).toBeGreaterThanOrEqual(READING_WIDTH_MIN_CH);
|
|
56
|
+
expect(READING_WIDTH_CH).toBeLessThanOrEqual(READING_WIDTH_MAX_CH);
|
|
57
|
+
expect(readingWidthStyle()).toEqual({ maxWidth: "70ch", width: "100%" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("clamps and supports eject", () => {
|
|
61
|
+
expect(readingWidthStyle(40)).toEqual({ maxWidth: "65ch", width: "100%" });
|
|
62
|
+
expect(readingWidthStyle(90)).toEqual({ maxWidth: "75ch", width: "100%" });
|
|
63
|
+
expect(readingWidthStyle("none")).toEqual({});
|
|
64
|
+
expect(readingWidthStyle(null)).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("press target (DG-LAY-04)", () => {
|
|
69
|
+
it("exports 44px floor + hitSlop expansion", () => {
|
|
70
|
+
expect(MIN_PRESS_TARGET).toBe(44);
|
|
71
|
+
expect(pressTargetStyle()).toEqual({ minWidth: 44, minHeight: 44 });
|
|
72
|
+
expect(pressTargetHitSlop(24)).toEqual({ top: 10, bottom: 10, left: 10, right: 10 });
|
|
73
|
+
expect(pressTargetHitSlop(44)).toEqual({ top: 0, bottom: 0, left: 0, right: 0 });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout tokens — DG-LAY-01..05 (W11).
|
|
3
|
+
*
|
|
4
|
+
* Canonical size classes for layout decisions (pane count, nav mode, overlay
|
|
5
|
+
* sheet vs float). Primitives consume these classes / helpers — not raw px in
|
|
6
|
+
* call sites.
|
|
7
|
+
*
|
|
8
|
+
* ## Canonical choice (2026-08-12)
|
|
9
|
+
*
|
|
10
|
+
* Hybrid of shipped MP1 behavior + Material window-size naming:
|
|
11
|
+
* - **Names** follow Material (compact → xl).
|
|
12
|
+
* - **Numbers** prefer already-shipped thresholds:
|
|
13
|
+
* - `medium` @ 640 = Tamagui `$sm` / DeskShell narrow (Fluent sm).
|
|
14
|
+
* - `expanded` @ 860 = FloatingPanel sheet↔float pivot (~Material 840).
|
|
15
|
+
* - `large` / `xl` = Tamagui `$lg` / `$xl`.
|
|
16
|
+
*
|
|
17
|
+
* Fluent (480/640/1024/1366/1920) and Material (600/840/1200/1600) stay in the
|
|
18
|
+
* compat table below for mapping — they are not second sources of truth.
|
|
19
|
+
*
|
|
20
|
+
* | Class | Min px | Max panes (default) | Tamagui media | Notes |
|
|
21
|
+
* |-----------|--------|---------------------|---------------|--------------------------------|
|
|
22
|
+
* | compact | 0 | 1 | < `$sm` | phones; drawer nav |
|
|
23
|
+
* | medium | 640 | 1 | `$sm` | DeskShell rail; still 1 pane |
|
|
24
|
+
* | expanded | 860 | 2 | — (custom) | FloatingPanel float; 2 panes |
|
|
25
|
+
* | large | 1024 | 2 | `$lg` | |
|
|
26
|
+
* | xl | 1280 | 2 | `$xl` | |
|
|
27
|
+
*
|
|
28
|
+
* Compat: Material compact≈our compact (600→640), expanded≈our expanded
|
|
29
|
+
* (840→860). Fluent sm=640 matches medium; Fluent lg=1024 matches large.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { useEffect, useState } from "react";
|
|
33
|
+
import { isWeb, useWindowDimensions } from "tamagui";
|
|
34
|
+
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
35
|
+
|
|
36
|
+
// ── Breakpoint tokens (DG-LAY-01) ─────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export type LayoutSizeClass = "compact" | "medium" | "expanded" | "large" | "xl";
|
|
39
|
+
|
|
40
|
+
/** Named min-widths. Prefer these over literals in layout code. */
|
|
41
|
+
export const layoutBreakpoints = {
|
|
42
|
+
/** End of compact / DeskShell narrow drawer threshold (Tamagui `$sm`). */
|
|
43
|
+
medium: 640,
|
|
44
|
+
/** Two-pane + FloatingPanel float threshold (~M3 expanded 840). */
|
|
45
|
+
expanded: 860,
|
|
46
|
+
/** Tamagui `$lg`. */
|
|
47
|
+
large: 1024,
|
|
48
|
+
/** Tamagui `$xl`. */
|
|
49
|
+
xl: 1280,
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
export type LayoutBreakpointName = keyof typeof layoutBreakpoints;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Alias kept for FloatingPanel / overlay consumers — same value as
|
|
56
|
+
* `layoutBreakpoints.expanded`.
|
|
57
|
+
*/
|
|
58
|
+
export const OVERLAY_BREAKPOINT = layoutBreakpoints.expanded;
|
|
59
|
+
|
|
60
|
+
/** DeskShell sidebar collapse — same value as `layoutBreakpoints.medium`. */
|
|
61
|
+
export const NARROW_BREAKPOINT = layoutBreakpoints.medium;
|
|
62
|
+
|
|
63
|
+
export const LAYOUT_SIZE_CLASS_ORDER: readonly LayoutSizeClass[] = [
|
|
64
|
+
"compact",
|
|
65
|
+
"medium",
|
|
66
|
+
"expanded",
|
|
67
|
+
"large",
|
|
68
|
+
"xl",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
/** Resolve a viewport width to the canonical layout size class. */
|
|
72
|
+
export function getLayoutSizeClass(width: number): LayoutSizeClass {
|
|
73
|
+
if (width >= layoutBreakpoints.xl) return "xl";
|
|
74
|
+
if (width >= layoutBreakpoints.large) return "large";
|
|
75
|
+
if (width >= layoutBreakpoints.expanded) return "expanded";
|
|
76
|
+
if (width >= layoutBreakpoints.medium) return "medium";
|
|
77
|
+
return "compact";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Default horizontal pane budget by size class (Material PaneScaffoldDirective). */
|
|
81
|
+
export function defaultMaxPanes(sizeClass: LayoutSizeClass): 1 | 2 {
|
|
82
|
+
return sizeClass === "compact" || sizeClass === "medium" ? 1 : 2;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function layoutSizeClassAtLeast(
|
|
86
|
+
current: LayoutSizeClass,
|
|
87
|
+
minimum: LayoutSizeClass,
|
|
88
|
+
): boolean {
|
|
89
|
+
return LAYOUT_SIZE_CLASS_ORDER.indexOf(current) >= LAYOUT_SIZE_CLASS_ORDER.indexOf(minimum);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Web viewport width with an SSR fallback (`medium`). Web-only callers. */
|
|
93
|
+
function readViewportWidth(): number {
|
|
94
|
+
if (typeof window === "undefined") return layoutBreakpoints.medium;
|
|
95
|
+
return window.innerWidth;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Live layout size class from viewport width — cross-platform:
|
|
100
|
+
* - Web: matchMedia listeners on the class thresholds (re-renders only when
|
|
101
|
+
* the class flips, not per resize pixel). SSR first paint → `medium`.
|
|
102
|
+
* - Native: react-native window dimensions (rotation / split-screen), so
|
|
103
|
+
* phones report `compact` instead of the former pinned `medium`.
|
|
104
|
+
*/
|
|
105
|
+
export function useLayoutSizeClass(): LayoutSizeClass {
|
|
106
|
+
// Hook order is unconditional; on web the value is ignored in favor of the
|
|
107
|
+
// matchMedia path below (threshold-only updates).
|
|
108
|
+
const nativeWidth = useWindowDimensions().width;
|
|
109
|
+
const [sizeClass, setSizeClass] = useState<LayoutSizeClass>(() =>
|
|
110
|
+
getLayoutSizeClass(isWeb ? readViewportWidth() : nativeWidth),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!isWeb || typeof window === "undefined") return;
|
|
115
|
+
|
|
116
|
+
const queries = [
|
|
117
|
+
window.matchMedia(`(min-width: ${layoutBreakpoints.xl}px)`),
|
|
118
|
+
window.matchMedia(`(min-width: ${layoutBreakpoints.large}px)`),
|
|
119
|
+
window.matchMedia(`(min-width: ${layoutBreakpoints.expanded}px)`),
|
|
120
|
+
window.matchMedia(`(min-width: ${layoutBreakpoints.medium}px)`),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const sync = () => setSizeClass(getLayoutSizeClass(window.innerWidth));
|
|
124
|
+
sync();
|
|
125
|
+
for (const mq of queries) mq.addEventListener("change", sync);
|
|
126
|
+
return () => {
|
|
127
|
+
for (const mq of queries) mq.removeEventListener("change", sync);
|
|
128
|
+
};
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
// Native: derive from the live dimensions; setState bails when unchanged.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (isWeb) return;
|
|
134
|
+
setSizeClass(getLayoutSizeClass(nativeWidth));
|
|
135
|
+
}, [nativeWidth]);
|
|
136
|
+
|
|
137
|
+
return sizeClass;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** True when the viewport may show two side-by-side panes. */
|
|
141
|
+
export function useMultiPane(maxPanes?: 1 | 2): boolean {
|
|
142
|
+
const sizeClass = useLayoutSizeClass();
|
|
143
|
+
const budget = maxPanes ?? defaultMaxPanes(sizeClass);
|
|
144
|
+
return budget >= 2 && defaultMaxPanes(sizeClass) >= 2;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Reading width (DG-LAY-05) ─────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/** Ideal prose measure (mid of GOV.UK ≤75 / USWDS ~66). */
|
|
150
|
+
export const READING_WIDTH_CH = 70;
|
|
151
|
+
export const READING_WIDTH_MIN_CH = 65;
|
|
152
|
+
export const READING_WIDTH_MAX_CH = 75;
|
|
153
|
+
|
|
154
|
+
export type ReadingWidthStyle = {
|
|
155
|
+
maxWidth: string;
|
|
156
|
+
width: "100%";
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** Style props for prose / long-form columns. Pass `null`/`"none"` to eject. */
|
|
160
|
+
export function readingWidthStyle(
|
|
161
|
+
measure: number | "none" | null = READING_WIDTH_CH,
|
|
162
|
+
): ReadingWidthStyle | Record<string, never> {
|
|
163
|
+
if (measure === "none" || measure === null) return {};
|
|
164
|
+
const ch = Math.min(READING_WIDTH_MAX_CH, Math.max(READING_WIDTH_MIN_CH, measure));
|
|
165
|
+
return { maxWidth: `${ch}ch`, width: "100%" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Min press target (DG-LAY-04) ──────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/** Platform floor for interactive targets (Fluent/HIG web+iOS 44; M3 48 on Android). */
|
|
171
|
+
export const MIN_PRESS_TARGET = 44;
|
|
172
|
+
|
|
173
|
+
export type PressTargetStyle = {
|
|
174
|
+
minWidth: number;
|
|
175
|
+
minHeight: number;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export function pressTargetStyle(size: number = MIN_PRESS_TARGET): PressTargetStyle {
|
|
179
|
+
return { minWidth: size, minHeight: size };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type HitSlopInsets = {
|
|
183
|
+
top: number;
|
|
184
|
+
bottom: number;
|
|
185
|
+
left: number;
|
|
186
|
+
right: number;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Expand hit area when the visual control is smaller than the floor. */
|
|
190
|
+
export function pressTargetHitSlop(
|
|
191
|
+
visualSize: number,
|
|
192
|
+
min: number = MIN_PRESS_TARGET,
|
|
193
|
+
): HitSlopInsets {
|
|
194
|
+
const pad = Math.max(0, Math.ceil((min - visualSize) / 2));
|
|
195
|
+
return { top: pad, bottom: pad, left: pad, right: pad };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Semantic gaps (DG-LAY-03) ─────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Semantic spacing roles map onto the space-knob recipes:
|
|
202
|
+
* `within-group` → `knobProps.gap`, `between-groups` → `knobProps.gapLg`.
|
|
203
|
+
*/
|
|
204
|
+
export function useSemanticGaps(): {
|
|
205
|
+
withinGroup: { gap: string };
|
|
206
|
+
betweenGroups: { gap: string };
|
|
207
|
+
} {
|
|
208
|
+
const { knobProps } = useResolvedKnobs();
|
|
209
|
+
return {
|
|
210
|
+
withinGroup: knobProps.gap,
|
|
211
|
+
betweenGroups: knobProps.gapLg,
|
|
212
|
+
};
|
|
213
|
+
}
|