@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
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { isWeb } from "@multiplatform.one/platform";
|
|
1
2
|
import type { TransitionProp } from "tamagui";
|
|
2
|
-
|
|
3
|
+
// Importing the class constant also mounts the corner-shape stylesheet at
|
|
4
|
+
// theme load (web only, idempotent) — see cornerSmoothing.ts.
|
|
5
|
+
import { cornerSmoothClassName } from "./cornerSmoothing";
|
|
6
|
+
import {
|
|
7
|
+
ensureFocusVisibleRing,
|
|
8
|
+
STATE_LAYER_HOVER,
|
|
9
|
+
STATE_LAYER_PRESS,
|
|
10
|
+
withDefaultStateLayer,
|
|
11
|
+
} from "./focusState";
|
|
12
|
+
import type { CornerSmoothing, Knobs, StateKnobs } from "./knobs";
|
|
3
13
|
import type {
|
|
4
14
|
ControlStateProps,
|
|
15
|
+
DisabledRecipe,
|
|
5
16
|
ElevationStateProps,
|
|
6
17
|
KnobProps,
|
|
7
18
|
KnobPropsOverride,
|
|
@@ -10,6 +21,42 @@ import type {
|
|
|
10
21
|
TextStateProps,
|
|
11
22
|
} from "./recipes";
|
|
12
23
|
|
|
24
|
+
// ── Density (compact) stepping ────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Step size down one level for compact density. */
|
|
27
|
+
export const compactSizeMap: Record<Knobs["size"], Knobs["size"]> = {
|
|
28
|
+
small: "small",
|
|
29
|
+
medium: "small",
|
|
30
|
+
large: "medium",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Step space down one level for compact density. */
|
|
34
|
+
export const compactSpaceMap: Record<Knobs["space"], Knobs["space"]> = {
|
|
35
|
+
small: "small",
|
|
36
|
+
medium: "small",
|
|
37
|
+
large: "medium",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply density mode to knobs before token resolution.
|
|
42
|
+
*
|
|
43
|
+
* - `density: "compact"` steps size + space down one level.
|
|
44
|
+
* - `compactOverride` wins over the density knob when provided:
|
|
45
|
+
* `true` forces compact stepping; `false` forces comfortable (no step).
|
|
46
|
+
*/
|
|
47
|
+
export function applyDensity(knobs: Knobs, compactOverride?: boolean): Knobs {
|
|
48
|
+
const useCompact = compactOverride !== undefined ? compactOverride : knobs.density === "compact";
|
|
49
|
+
if (!useCompact) {
|
|
50
|
+
return knobs.density === "comfortable" ? knobs : { ...knobs, density: "comfortable" };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...knobs,
|
|
54
|
+
density: "compact",
|
|
55
|
+
size: compactSizeMap[knobs.size],
|
|
56
|
+
space: compactSpaceMap[knobs.space],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
13
60
|
// ── Knob-to-token mapping tables ──────────────────────────────
|
|
14
61
|
|
|
15
62
|
const borderRadiusMap = {
|
|
@@ -35,22 +82,74 @@ const borderWidthMap = {
|
|
|
35
82
|
large: 2,
|
|
36
83
|
} as const;
|
|
37
84
|
|
|
85
|
+
// ── Corner smoothing (Axiom 15 OPTICS) ────────────────────────
|
|
86
|
+
// `smooth` upgrades circular-arc corners to continuous curvature —
|
|
87
|
+
// `corner-shape: squircle` (≡ superellipse(2), ≈ Apple / Figma 0.6) — as a
|
|
88
|
+
// web progressive enhancement: Chromium 139+ paints it, every other engine
|
|
89
|
+
// drops the unknown declaration and keeps the plain border-radius arc.
|
|
90
|
+
// Delivery is a class + mounted stylesheet (cornerSmoothing.ts) because
|
|
91
|
+
// Tamagui strips unknown keys from the `style` prop (skipProps) but merges
|
|
92
|
+
// `className`. Native resolves to `round` for now (SquircleView adoption is
|
|
93
|
+
// phase 2); GTK has no corner-shape equivalent and stays on the plain arc.
|
|
94
|
+
const cornerSmoothingStyle: Record<CornerSmoothing, { className: string } | undefined> = isWeb
|
|
95
|
+
? { round: undefined, smooth: { className: cornerSmoothClassName } }
|
|
96
|
+
: { round: undefined, smooth: undefined };
|
|
97
|
+
|
|
38
98
|
const panelPaddingMap = {
|
|
39
|
-
small: "$
|
|
40
|
-
medium: "$
|
|
41
|
-
large: "$
|
|
99
|
+
small: "$3",
|
|
100
|
+
medium: "$4",
|
|
101
|
+
large: "$6",
|
|
42
102
|
} as const;
|
|
43
103
|
|
|
104
|
+
// ── Container radius cap (Axiom 1 CLIP) ──────────────────────
|
|
105
|
+
// A padded container's effective radius must not exceed its own padding:
|
|
106
|
+
// past that, the corner curve sweeps through the cell where children sit
|
|
107
|
+
// (radius > padding clips child corners). Px values mirror the
|
|
108
|
+
// @tamagui/themes tokens the maps above resolve to (radius $0/$2/$4/$6/$12
|
|
109
|
+
// = 0/5/9/16/50; space $3/$4/$6 = 13/18/32) — resolveKnobs stays a pure
|
|
110
|
+
// module, so the comparison happens on mirrored numbers, not live tokens.
|
|
111
|
+
// The cap only bites at `full` (every space) and `large` under
|
|
112
|
+
// `space=small`; default knobs are unchanged.
|
|
113
|
+
|
|
114
|
+
const radiusPxMap: Record<Knobs["borderRadius"], number> = {
|
|
115
|
+
none: 0,
|
|
116
|
+
small: 5,
|
|
117
|
+
medium: 9,
|
|
118
|
+
large: 16,
|
|
119
|
+
full: 50,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const panelPaddingPxMap: Record<Knobs["space"], number> = {
|
|
123
|
+
small: 13,
|
|
124
|
+
medium: 18,
|
|
125
|
+
large: 32,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Cap a container's radius at its own padding (Axiom 1 CLIP: "my children's
|
|
130
|
+
* pixel clips first → cap at padding"). Returns the radius token untouched
|
|
131
|
+
* while it fits, or the padding px number once the token would exceed it.
|
|
132
|
+
*/
|
|
133
|
+
export function capContainerRadius(
|
|
134
|
+
borderRadius: Knobs["borderRadius"],
|
|
135
|
+
space: Knobs["space"],
|
|
136
|
+
): string | number {
|
|
137
|
+
const radiusPx = radiusPxMap[borderRadius];
|
|
138
|
+
const paddingPx = panelPaddingPxMap[space];
|
|
139
|
+
return radiusPx > paddingPx ? paddingPx : borderRadiusMap[borderRadius];
|
|
140
|
+
}
|
|
141
|
+
|
|
44
142
|
const gapMap = {
|
|
45
143
|
small: "$2",
|
|
46
|
-
medium: "$
|
|
47
|
-
large: "$
|
|
144
|
+
medium: "$4",
|
|
145
|
+
large: "$5",
|
|
48
146
|
} as const;
|
|
49
147
|
|
|
148
|
+
// gapLg stays one step above gapMap.
|
|
50
149
|
const gapLgMap = {
|
|
51
150
|
small: "$3",
|
|
52
|
-
medium: "$
|
|
53
|
-
large: "$
|
|
151
|
+
medium: "$5",
|
|
152
|
+
large: "$6",
|
|
54
153
|
} as const;
|
|
55
154
|
|
|
56
155
|
const sizeTokenMap = {
|
|
@@ -92,19 +191,85 @@ const fontWeightMap = {
|
|
|
92
191
|
bold: "700",
|
|
93
192
|
} as const;
|
|
94
193
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
194
|
+
// Web keeps Tamagui size-token strings — Tamagui resolves them to boxShadow.
|
|
195
|
+
// Native must emit NUMBERS: knobProps.elevation gets spread onto plain RN
|
|
196
|
+
// Views by non-Tamagui consumers, and Android's RCTView `elevation` prop is a
|
|
197
|
+
// Double — a token string kills the ReactHost with "java.lang.String cannot
|
|
198
|
+
// be cast to java.lang.Double" (SB-N-04). The numbers are the exact
|
|
199
|
+
// tokens.size values Tamagui resolves those tokens to ($1=20, $2=28, $4=44),
|
|
200
|
+
// so Tamagui consumers render identical shadows either way.
|
|
201
|
+
const elevationMap: Record<string, string | number | undefined> = isWeb
|
|
202
|
+
? { none: undefined, small: "$1", medium: "$2", large: "$4" }
|
|
203
|
+
: { none: undefined, small: 20, medium: 28, large: 44 };
|
|
101
204
|
|
|
205
|
+
// Legibility floor: body/label text must never land below ~4.5:1 against its
|
|
206
|
+
// surface. $color11 is the lowest ramp step that clears AA on color1/2
|
|
207
|
+
// surfaces, so "low" clamps there ($color9 measured 1.29-3.43:1 — SB-R-01).
|
|
102
208
|
const textAccentColorMap: Record<string, string> = {
|
|
103
|
-
low: "$
|
|
209
|
+
low: "$color11",
|
|
104
210
|
medium: "$color11",
|
|
105
211
|
high: "$color",
|
|
106
212
|
};
|
|
107
213
|
|
|
214
|
+
// ── Disabled treatment (LC-40 DISABLED-VISIBLE) ───────────────
|
|
215
|
+
// A disabled control must be visually distinguishable from its enabled twin
|
|
216
|
+
// under BOTH `disabledStyle` values (Axiom 6 HONEST STATE; DG-ST-05 names
|
|
217
|
+
// the dial: Fluent keepLabel / Material dimWhole). Tiers are theme-relative
|
|
218
|
+
// ramp steps so intent sub-themes desaturate within their own hue instead
|
|
219
|
+
// of fighting it:
|
|
220
|
+
// - keepLabel (default): the control CHROME washes out while label/value
|
|
221
|
+
// text keeps ≥ AA. Text-bearing chrome swaps fill/border to the muted
|
|
222
|
+
// tiers ($color3 wash — the native `input:disabled` idiom — bordered by
|
|
223
|
+
// $color6) with NO opacity so inner text, re-anchored to $color11 (the
|
|
224
|
+
// documented lowest AA step, see textAccentColorMap above), stays ≥4.5:1.
|
|
225
|
+
// Text-free chrome (track/box/ring/star) opacity-dims at 0.5 — the value
|
|
226
|
+
// the catalog's hand-rolled treatments already shipped.
|
|
227
|
+
// - dimWhole: the whole ASSEMBLY (label included) dims at 0.5; control
|
|
228
|
+
// fragments then carry only the not-allowed affordance so the dim is
|
|
229
|
+
// applied once, by the assembly owner.
|
|
230
|
+
|
|
231
|
+
const DISABLED_CHROME_OPACITY = 0.5;
|
|
232
|
+
const DISABLED_ASSEMBLY_OPACITY = 0.5;
|
|
233
|
+
|
|
234
|
+
const disabledCursor = { cursor: "not-allowed" as const };
|
|
235
|
+
|
|
236
|
+
// Selected-while-disabled tier: one ramp step stronger than the plain wash
|
|
237
|
+
// ($color3/$color6) so a chosen segment stays visibly chosen (Axiom 6 —
|
|
238
|
+
// owner-ratified recipe-level "selected wash"). Carried under BOTH
|
|
239
|
+
// disabledStyle values: consumers drop their live active fill when disabled
|
|
240
|
+
// (the `active` variant beats the wash), so without this tier a dimWhole
|
|
241
|
+
// assembly dim alone could not distinguish selection either.
|
|
242
|
+
const disabledSelectedSurface = {
|
|
243
|
+
backgroundColor: "$color5",
|
|
244
|
+
borderColor: "$color8",
|
|
245
|
+
...disabledCursor,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function resolveDisabled(disabledStyle: Knobs["disabledStyle"]): DisabledRecipe {
|
|
249
|
+
if (disabledStyle === "dimWhole") {
|
|
250
|
+
return {
|
|
251
|
+
surfaceKnobProps: { ...disabledCursor },
|
|
252
|
+
selectedSurfaceKnobProps: { ...disabledSelectedSurface },
|
|
253
|
+
chromeKnobProps: { ...disabledCursor },
|
|
254
|
+
textKnobProps: {},
|
|
255
|
+
assemblyKnobProps: { opacity: DISABLED_ASSEMBLY_OPACITY },
|
|
256
|
+
style: disabledStyle,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
surfaceKnobProps: {
|
|
261
|
+
backgroundColor: "$color3",
|
|
262
|
+
borderColor: "$color6",
|
|
263
|
+
...disabledCursor,
|
|
264
|
+
},
|
|
265
|
+
selectedSurfaceKnobProps: { ...disabledSelectedSurface },
|
|
266
|
+
chromeKnobProps: { opacity: DISABLED_CHROME_OPACITY, ...disabledCursor },
|
|
267
|
+
textKnobProps: { color: "$color11" },
|
|
268
|
+
assemblyKnobProps: {},
|
|
269
|
+
style: disabledStyle,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
108
273
|
// ── State style resolver ──────────────────────────────────────
|
|
109
274
|
|
|
110
275
|
function resolveStateKnobs(state: StateKnobs): {
|
|
@@ -159,10 +324,32 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
159
324
|
const bodyFontValue = bodyFontMap[knobs.bodyFont] ?? "$body";
|
|
160
325
|
const transition = knobs.animation === "none" ? undefined : (knobs.animation as TransitionProp);
|
|
161
326
|
|
|
327
|
+
const containerRadiusValue = capContainerRadius(knobs.borderRadius, knobs.space);
|
|
328
|
+
|
|
329
|
+
// Corner smoothing rides every radius-bearing fragment so consumers need
|
|
330
|
+
// zero edits (KNOB-TOTALITY, LC-18). Identity guard (Axiom 3 NULL +
|
|
331
|
+
// R-BINARY/R-PILL): `none` has no arc to smooth, and at `full` the
|
|
332
|
+
// uncapped token turns control-tier parts into circles/capsules whose
|
|
333
|
+
// shape is identity — squircle ends would distort them — so the
|
|
334
|
+
// raw-radius fragments only smooth the arc classes (small/medium/large).
|
|
335
|
+
// Capped container fragments stay plain arcs at every knob (the Axiom 1
|
|
336
|
+
// cap = padding px, which superellipse corners under-fill, so the cap
|
|
337
|
+
// remains a safe over-approximation) and smooth whenever a nonzero arc
|
|
338
|
+
// exists. featureSurface's radius is fixed ($10, knob-independent), so it
|
|
339
|
+
// smooths whenever the knob says smooth.
|
|
340
|
+
const smoothing = cornerSmoothingStyle[knobs.cornerSmoothing ?? "round"];
|
|
341
|
+
const scaleSmoothing =
|
|
342
|
+
knobs.borderRadius === "none" || knobs.borderRadius === "full" ? undefined : smoothing;
|
|
343
|
+
const containerSmoothing = knobs.borderRadius === "none" ? undefined : smoothing;
|
|
344
|
+
|
|
162
345
|
const outlined = knobs.fillStyle === "outlined";
|
|
163
346
|
const elevationToken = elevationMap[knobs.elevation];
|
|
164
347
|
const textAccentColor = textAccentColorMap[knobs.textAccent] ?? "$color";
|
|
165
|
-
|
|
348
|
+
// One input surface (DF-19 / Axiom 5 ONE SOURCE): the knob resolves to the
|
|
349
|
+
// same tier forms' InputContainer uses (formInputColors.background.base =
|
|
350
|
+
// $color2), so Timeline's composer, RichEditor, and form fields agree on
|
|
351
|
+
// what an input surface is. $color6 forked a darker second voice.
|
|
352
|
+
const inputBackground = outlined ? "transparent" : "$color2";
|
|
166
353
|
|
|
167
354
|
const hover = resolveStateKnobs(knobs.hover);
|
|
168
355
|
const press = resolveStateKnobs(knobs.press);
|
|
@@ -172,11 +359,13 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
172
359
|
const nonEmpty = <T extends object>(obj: T): T | undefined =>
|
|
173
360
|
Object.keys(obj).length > 0 ? obj : undefined;
|
|
174
361
|
|
|
362
|
+
// DG-STATE-01: hover/press always carry on-color state-layer fills.
|
|
363
|
+
// DG-A11Y-01: focus-visible ring is always present and non-removable.
|
|
175
364
|
const control: KnobRecipe<ControlStateProps> = {
|
|
176
|
-
hoverKnobProps:
|
|
177
|
-
pressKnobProps:
|
|
365
|
+
hoverKnobProps: withDefaultStateLayer(hover.control, STATE_LAYER_HOVER),
|
|
366
|
+
pressKnobProps: withDefaultStateLayer(press.control, STATE_LAYER_PRESS),
|
|
178
367
|
focusKnobProps: nonEmpty(focus.control),
|
|
179
|
-
focusVisibleKnobProps:
|
|
368
|
+
focusVisibleKnobProps: ensureFocusVisibleRing(focusVisible.control),
|
|
180
369
|
};
|
|
181
370
|
const text: KnobRecipe<TextStateProps> = {
|
|
182
371
|
hoverKnobProps: nonEmpty(hover.text),
|
|
@@ -192,10 +381,49 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
192
381
|
};
|
|
193
382
|
|
|
194
383
|
const knobProps: KnobProps = {
|
|
195
|
-
borderRadius: {
|
|
196
|
-
|
|
384
|
+
borderRadius: {
|
|
385
|
+
borderRadius: radiusToken,
|
|
386
|
+
borderColor: "$borderColor",
|
|
387
|
+
borderWidth: borderWidthValue,
|
|
388
|
+
...scaleSmoothing,
|
|
389
|
+
},
|
|
390
|
+
borderRadiusOuter: { borderRadius: outerRadiusToken, ...containerSmoothing },
|
|
391
|
+
// Axiom 1 child-clip cap: padded containers (Card/Panel/DialogContent)
|
|
392
|
+
// spread this after their radius-bearing fragment. Non-container parts
|
|
393
|
+
// keep the uncapped `borderRadius` fragment.
|
|
394
|
+
containerRadius: { borderRadius: containerRadiusValue, ...containerSmoothing },
|
|
197
395
|
elevation: elevationToken,
|
|
198
|
-
surface: {
|
|
396
|
+
surface: {
|
|
397
|
+
backgroundColor: "$color1",
|
|
398
|
+
borderColor: "$borderColor",
|
|
399
|
+
borderWidth: borderWidthValue,
|
|
400
|
+
},
|
|
401
|
+
cardSurface: {
|
|
402
|
+
backgroundColor: "$color1",
|
|
403
|
+
borderRadius: containerRadiusValue,
|
|
404
|
+
padding: paddingToken,
|
|
405
|
+
...containerSmoothing,
|
|
406
|
+
},
|
|
407
|
+
elevatedSurface: {
|
|
408
|
+
backgroundColor: "$color1",
|
|
409
|
+
borderColor: "$borderColor",
|
|
410
|
+
borderWidth: borderWidthValue,
|
|
411
|
+
borderRadius: radiusToken,
|
|
412
|
+
elevation: elevationToken,
|
|
413
|
+
...scaleSmoothing,
|
|
414
|
+
},
|
|
415
|
+
featureSurface: {
|
|
416
|
+
backgroundColor: "$color1",
|
|
417
|
+
borderRadius: "$10",
|
|
418
|
+
padding: "$6",
|
|
419
|
+
// Bento marketing/auth card shadow: ~0px 9px 12px rgba(0,0,0,0.02) —
|
|
420
|
+
// softer/wider than any elevationMap token, so explicit shadow props.
|
|
421
|
+
shadowColor: "rgba(0,0,0,0.02)",
|
|
422
|
+
shadowOffset: { width: 0, height: 9 },
|
|
423
|
+
shadowOpacity: 1,
|
|
424
|
+
shadowRadius: 12,
|
|
425
|
+
...smoothing,
|
|
426
|
+
},
|
|
199
427
|
inputSurface: { borderWidth: borderWidthValue },
|
|
200
428
|
panelPadding: { padding: paddingToken },
|
|
201
429
|
gap: { gap: gapToken },
|
|
@@ -218,9 +446,25 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
218
446
|
inputBackground,
|
|
219
447
|
space: knobs.space,
|
|
220
448
|
size: knobs.size,
|
|
449
|
+
density: knobs.density,
|
|
450
|
+
// House decisions — behavioral pass-through (no token mapping).
|
|
451
|
+
fieldLabelPlacement: knobs.fieldLabelPlacement,
|
|
452
|
+
requiredMarking: knobs.requiredMarking,
|
|
453
|
+
tableZebra: knobs.tableZebra,
|
|
454
|
+
bulkBarPlacement: knobs.bulkBarPlacement,
|
|
455
|
+
selectAllScope: knobs.selectAllScope,
|
|
456
|
+
timestampStyle: knobs.timestampStyle,
|
|
457
|
+
disabledStyle: knobs.disabledStyle,
|
|
458
|
+
formAutofocus: knobs.formAutofocus,
|
|
221
459
|
};
|
|
222
460
|
|
|
223
461
|
const finalKnobProps = override ? { ...knobProps, ...override(knobProps, knobs) } : knobProps;
|
|
224
462
|
|
|
225
|
-
return {
|
|
463
|
+
return {
|
|
464
|
+
knobProps: finalKnobProps,
|
|
465
|
+
control,
|
|
466
|
+
text,
|
|
467
|
+
elevation,
|
|
468
|
+
disabledState: resolveDisabled(knobs.disabledStyle),
|
|
469
|
+
};
|
|
226
470
|
}
|
package/src/theme/shared.tsx
CHANGED
|
@@ -8,12 +8,35 @@ import type { Preset } from "./preset.types";
|
|
|
8
8
|
|
|
9
9
|
const presetRegistry = new Map<string, Preset>();
|
|
10
10
|
|
|
11
|
+
/** Unknown-preset names already warned about, so a render loop doesn't spam. */
|
|
12
|
+
const warnedUnknownPresets = new Set<string>();
|
|
13
|
+
|
|
11
14
|
export function registerPreset(name: string, preset: Preset): void {
|
|
12
15
|
presetRegistry.set(name, preset);
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Look up a registered preset by name.
|
|
20
|
+
*
|
|
21
|
+
* A requested-but-unregistered name is a silent bug otherwise: callers just
|
|
22
|
+
* fall back to the default preset, so e.g. `preset:material` renders
|
|
23
|
+
* byte-identical to `default` with no signal. Warn ONCE per unknown name
|
|
24
|
+
* (empty string and the implicit "default" are legitimate no-ops and stay
|
|
25
|
+
* quiet) so a typo'd or unregistered preset is loud instead of invisible.
|
|
26
|
+
*/
|
|
15
27
|
export function getPreset(name: string): Preset | undefined {
|
|
16
|
-
|
|
28
|
+
const preset = presetRegistry.get(name);
|
|
29
|
+
if (!preset && name && name !== "default" && !warnedUnknownPresets.has(name)) {
|
|
30
|
+
warnedUnknownPresets.add(name);
|
|
31
|
+
const known = getPresetNames();
|
|
32
|
+
console.warn(
|
|
33
|
+
`[@multiplatform.one/theme] Unknown preset "${name}" is not registered — ` +
|
|
34
|
+
`falling back to the default preset. Registered presets: ${
|
|
35
|
+
known.length ? known.join(", ") : "(none)"
|
|
36
|
+
}. Register it with registerPreset("${name}", …) or pass a known name.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return preset;
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
export function getPresetNames(): string[] {
|
|
@@ -22,6 +45,7 @@ export function getPresetNames(): string[] {
|
|
|
22
45
|
|
|
23
46
|
export function clearPresets(): void {
|
|
24
47
|
presetRegistry.clear();
|
|
48
|
+
warnedUnknownPresets.clear();
|
|
25
49
|
}
|
|
26
50
|
|
|
27
51
|
// ── Preset resolution ─────────────────────────────────────────────────────
|
|
@@ -34,7 +58,7 @@ export function clearPresets(): void {
|
|
|
34
58
|
* using `defaultKnobs`.
|
|
35
59
|
*/
|
|
36
60
|
export function resolveThemeFromPreset(presetName: string, overrides: Partial<Knobs>): Preset {
|
|
37
|
-
const preset =
|
|
61
|
+
const preset = getPreset(presetName);
|
|
38
62
|
const base: Preset = preset ?? {
|
|
39
63
|
theme: "" as ThemeName,
|
|
40
64
|
knobs: { ...defaultKnobs },
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createContext, useContext, useMemo } from "react";
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import type { SizeTokens } from "tamagui";
|
|
3
3
|
import { defaultKnobs, type Knobs } from "./knobs";
|
|
4
4
|
import type { IntentOverride } from "./preset.types";
|
|
5
5
|
import type { KnobPropsOverride, ResolvedKnobs } from "./recipes";
|
|
6
|
-
import { resolveKnobs } from "./resolveKnobs";
|
|
6
|
+
import { applyDensity, resolveKnobs } from "./resolveKnobs";
|
|
7
7
|
import { usePresetContext } from "./PresetContext";
|
|
8
8
|
import { defaultIntents } from "./intents";
|
|
9
9
|
|
|
@@ -13,13 +13,41 @@ export function useIntentContext(): string | null {
|
|
|
13
13
|
return useContext(IntentContext);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** OS `prefers-reduced-motion: reduce` — forces animation=none for A-* decorative motion. */
|
|
17
|
+
function usePrefersReducedMotion(): boolean {
|
|
18
|
+
const [reduced, setReduced] = useState(() => {
|
|
19
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
20
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
21
|
+
});
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
24
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
25
|
+
const handler = () => setReduced(mq.matches);
|
|
26
|
+
handler();
|
|
27
|
+
// RN / older WebKit: MediaQueryList has addListener, not addEventListener
|
|
28
|
+
if (typeof mq.addEventListener === "function") {
|
|
29
|
+
mq.addEventListener("change", handler);
|
|
30
|
+
return () => mq.removeEventListener("change", handler);
|
|
31
|
+
}
|
|
32
|
+
if (typeof mq.addListener === "function") {
|
|
33
|
+
mq.addListener(handler);
|
|
34
|
+
return () => mq.removeListener?.(handler);
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
37
|
+
return reduced;
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
export interface UseResolvedKnobsOptions {
|
|
17
41
|
intent?: string;
|
|
18
42
|
component?: string;
|
|
19
43
|
override?: KnobPropsOverride;
|
|
20
44
|
/** Override the size from knobs */
|
|
21
45
|
size?: SizeTokens;
|
|
22
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Compact sizing override. When set, wins over `knobs.density`:
|
|
48
|
+
* `true` forces compact stepping; `false` forces comfortable.
|
|
49
|
+
* When omitted, the density knob controls compact vs comfortable.
|
|
50
|
+
*/
|
|
23
51
|
compact?: boolean;
|
|
24
52
|
}
|
|
25
53
|
|
|
@@ -34,19 +62,13 @@ const sizeTokenToKnobSize: Record<string, Knobs["size"]> = {
|
|
|
34
62
|
$true: "medium",
|
|
35
63
|
};
|
|
36
64
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Step down space for compact mode
|
|
45
|
-
const compactSpaceMap: Record<Knobs["space"], Knobs["space"]> = {
|
|
46
|
-
small: "small",
|
|
47
|
-
medium: "small",
|
|
48
|
-
large: "medium",
|
|
49
|
-
};
|
|
65
|
+
// Semantic intents whose Buttons render SOLID (see defaults/builderOptions):
|
|
66
|
+
// the _Button sub-theme surfaces on the hue's step 11 and carries a
|
|
67
|
+
// luminance-computed on-fill foreground as its `color` token (Axiom 15 /
|
|
68
|
+
// LC-05), so filled labels bind `$color` and outlined labels pin to the
|
|
69
|
+
// hue's readable text tier instead of the on-fill foreground. Exported so
|
|
70
|
+
// Button can anchor outlined frames (icon currentColor) the same way.
|
|
71
|
+
export const solidButtonIntents = new Set(["error", "warning", "success"]);
|
|
50
72
|
|
|
51
73
|
/**
|
|
52
74
|
* Resolve knobs → { knobProps }.
|
|
@@ -56,12 +78,14 @@ const compactSpaceMap: Record<Knobs["space"], Knobs["space"]> = {
|
|
|
56
78
|
* 2. Merge intent-level overrides (if intent provided or from context)
|
|
57
79
|
* 3. Merge intent+component overrides (if both provided)
|
|
58
80
|
* 4. Apply size override if provided
|
|
59
|
-
* 5. Apply
|
|
60
|
-
* 6.
|
|
81
|
+
* 5. Apply density (knobs.density, or compact option which wins)
|
|
82
|
+
* 6. OS reduced-motion forces animation="none"
|
|
83
|
+
* 7. Call resolveKnobs(mergedKnobs, override) — override merges last
|
|
61
84
|
*/
|
|
62
85
|
export function useResolvedKnobs(options?: UseResolvedKnobsOptions): ResolvedKnobs {
|
|
63
86
|
const presetCtx = usePresetContext();
|
|
64
87
|
const contextIntent = useIntentContext();
|
|
88
|
+
const prefersReducedMotion = usePrefersReducedMotion();
|
|
65
89
|
|
|
66
90
|
const intent = options?.intent ?? contextIntent ?? undefined;
|
|
67
91
|
const component = options?.component;
|
|
@@ -107,15 +131,32 @@ export function useResolvedKnobs(options?: UseResolvedKnobsOptions): ResolvedKno
|
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
133
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
// Density knob or per-call compact override (override wins when provided)
|
|
135
|
+
merged = applyDensity(merged, compact);
|
|
136
|
+
|
|
137
|
+
// OS reduced-motion ≡ animation=none for decorative A-* motion (knob consumers).
|
|
138
|
+
if (prefersReducedMotion) {
|
|
139
|
+
merged = { ...merged, animation: "none" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const resolved = resolveKnobs(merged, override);
|
|
143
|
+
|
|
144
|
+
// Semantic-intent Button labels: filled binds `$color` — the sub-theme's
|
|
145
|
+
// luminance-computed on-fill foreground (defaults/builderOptions, Axiom
|
|
146
|
+
// 15 / LC-05) — while outlined pins the hue's readable text tier
|
|
147
|
+
// ($color11 falls through the narrow _Button sub-theme to the intent
|
|
148
|
+
// theme), since the on-fill foreground is illegible on page surfaces.
|
|
149
|
+
if (intent && component === "Button" && solidButtonIntents.has(intent)) {
|
|
150
|
+
const labelColor = merged.fillStyle === "filled" ? "$color" : "$color11";
|
|
151
|
+
return {
|
|
152
|
+
...resolved,
|
|
153
|
+
knobProps: {
|
|
154
|
+
...resolved.knobProps,
|
|
155
|
+
body: { ...resolved.knobProps.body, color: labelColor },
|
|
156
|
+
},
|
|
116
157
|
};
|
|
117
158
|
}
|
|
118
159
|
|
|
119
|
-
return
|
|
120
|
-
}, [presetCtx, intent, component, override, sizeOverride, compact]);
|
|
160
|
+
return resolved;
|
|
161
|
+
}, [presetCtx, intent, component, override, sizeOverride, compact, prefersReducedMotion]);
|
|
121
162
|
}
|