@multiplatform.one/theme 7.4.0 → 7.5.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 +2 -1
- package/package.json +5 -5
- package/src/figma/figmaTokens.spec.ts +25 -21
- package/src/figma/figmaTokens.ts +2 -2
- package/src/font.ts +6 -19
- package/src/keyboardFocusRing.ts +12 -6
- package/src/theme/FontKnobStyles.tsx +5 -3
- package/src/theme/Tint.tsx +3 -2
- package/src/theme/animations/css.ts +14 -1
- package/src/theme/colorRules.spec.ts +233 -2
- package/src/theme/colorRules.ts +118 -0
- package/src/theme/cornerSmoothing.native.ts +39 -2
- package/src/theme/cornerSmoothing.spec.ts +213 -3
- package/src/theme/cornerSmoothing.ts +132 -10
- package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
- package/src/theme/createDefaultThemeConfig.ts +27 -8
- package/src/theme/defaults/builderOptions.ts +23 -10
- package/src/theme/defaults/categoryFonts.ts +21 -0
- package/src/theme/defaults/fonts.spec.ts +93 -10
- package/src/theme/defaults/fonts.theme.spec.tsx +128 -0
- package/src/theme/defaults/fonts.ts +155 -27
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
- package/src/theme/focusState.spec.ts +110 -1
- package/src/theme/focusState.ts +32 -7
- package/src/theme/fontCategoryStacks.ts +24 -0
- package/src/theme/fontKnobStyles.spec.ts +15 -2
- package/src/theme/glyphPaint.spec.ts +26 -0
- package/src/theme/glyphPaint.ts +46 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/knobs.ts +18 -2
- package/src/theme/layoutTokens.spec.ts +3 -1
- package/src/theme/layoutTokens.ts +47 -4
- package/src/theme/mpo13Proof.spec.ts +3 -3
- package/src/theme/mpo19Proof.spec.ts +285 -0
- package/src/theme/presets.ts +1 -1
- package/src/theme/radiusClass.spec.ts +394 -0
- package/src/theme/radiusClass.ts +203 -4
- package/src/theme/radiusClassTable.generated.ts +121 -0
- package/src/theme/recipeInputs.ts +223 -36
- package/src/theme/recipes.ts +68 -4
- package/src/theme/resolveKnobs.spec.ts +139 -11
- package/src/theme/resolveKnobs.ts +190 -25
- package/src/theme/sizeLadder.spec.ts +84 -0
- package/src/theme/sizeRecipes.spec.ts +19 -6
- package/src/theme/sizeRecipes.ts +43 -13
- package/src/theme/theme.native.tsx +2 -0
- package/src/theme/theme.provider.spec.ts +9 -0
- package/src/theme/theme.tsx +6 -3
- package/src/theme/tint.spec.tsx +36 -35
- package/src/theme/useResolvedKnobs.ts +6 -3
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/font.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +11 -5
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts +2 -1
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Tint.d.ts +2 -1
- package/types/theme/Tint.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +15 -2
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +59 -1
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.d.ts +42 -20
- package/types/theme/cornerSmoothing.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.native.d.ts +22 -2
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +21 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +19 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +135 -2
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +32 -6
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/fontCategoryStacks.d.ts +11 -0
- package/types/theme/fontCategoryStacks.d.ts.map +1 -0
- package/types/theme/glyphPaint.d.ts +16 -0
- package/types/theme/glyphPaint.d.ts.map +1 -0
- package/types/theme/index.d.ts +3 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +10 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +46 -4
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +96 -4
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +146 -0
- package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +85 -14
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +78 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +12 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +27 -14
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/useResolvedKnobs.d.ts +2 -2
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED from docs/design-guidelines.md §13 "Radius resolution" (DG-RAD-01..05)
|
|
3
|
+
* by scripts/radius-class-registry.mjs — DO NOT EDIT BY HAND.
|
|
4
|
+
*
|
|
5
|
+
* Regenerate: node scripts/radius-class-registry.mjs --emit
|
|
6
|
+
* Verify: node scripts/radius-class-registry.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* The doctrine is the source; this module is the resolver's copy of it. Editing the
|
|
9
|
+
* resolution here instead of in the doctrine is exactly the drift the generator exists
|
|
10
|
+
* to prevent (the same contract as scripts/radius-identity-registry.mjs).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const RADIUS_RESOLUTION_STOPS = ["none", "small", "medium", "large", "full"] as const;
|
|
14
|
+
|
|
15
|
+
/** The `borderRadius` knob stops as the doctrine table spells them. */
|
|
16
|
+
export type RadiusResolutionStop = (typeof RADIUS_RESOLUTION_STOPS)[number];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One doctrine table cell. `px` is a literal resolved radius; `half-height` is the
|
|
20
|
+
* part's own painted height over two (a true circle/pill); `min-token-padding` and
|
|
21
|
+
* `padding` are the CONTAINER-CAP spellings (Axiom 1 CLIP: never exceed own padding).
|
|
22
|
+
*/
|
|
23
|
+
export type RadiusResolutionCell =
|
|
24
|
+
| { readonly kind: "px"; readonly px: number }
|
|
25
|
+
| { readonly kind: "half-height" }
|
|
26
|
+
| { readonly kind: "min-token-padding"; readonly tokenPx: number }
|
|
27
|
+
| { readonly kind: "padding" };
|
|
28
|
+
|
|
29
|
+
export type RadiusResolutionRow = Readonly<Record<RadiusResolutionStop, RadiusResolutionCell>>;
|
|
30
|
+
|
|
31
|
+
/** DG-RAD-01..04 — resolved radius per class per stop. */
|
|
32
|
+
export const RADIUS_RESOLUTION_TABLE = {
|
|
33
|
+
DEFAULT: {
|
|
34
|
+
none: { kind: "px", px: 0 },
|
|
35
|
+
small: { kind: "px", px: 5 },
|
|
36
|
+
medium: { kind: "px", px: 9 },
|
|
37
|
+
large: { kind: "px", px: 16 },
|
|
38
|
+
full: { kind: "px", px: 50 },
|
|
39
|
+
},
|
|
40
|
+
"CIRCULAR-AT-FULL": {
|
|
41
|
+
none: { kind: "px", px: 0 },
|
|
42
|
+
small: { kind: "px", px: 5 },
|
|
43
|
+
medium: { kind: "px", px: 9 },
|
|
44
|
+
large: { kind: "px", px: 16 },
|
|
45
|
+
full: { kind: "half-height" },
|
|
46
|
+
},
|
|
47
|
+
BINARY: {
|
|
48
|
+
none: { kind: "px", px: 0 },
|
|
49
|
+
small: { kind: "half-height" },
|
|
50
|
+
medium: { kind: "half-height" },
|
|
51
|
+
large: { kind: "half-height" },
|
|
52
|
+
full: { kind: "half-height" },
|
|
53
|
+
},
|
|
54
|
+
"CONTAINER-CAP": {
|
|
55
|
+
none: { kind: "px", px: 0 },
|
|
56
|
+
small: { kind: "min-token-padding", tokenPx: 5 },
|
|
57
|
+
medium: { kind: "min-token-padding", tokenPx: 9 },
|
|
58
|
+
large: { kind: "min-token-padding", tokenPx: 16 },
|
|
59
|
+
full: { kind: "padding" },
|
|
60
|
+
},
|
|
61
|
+
} as const satisfies Record<string, RadiusResolutionRow>;
|
|
62
|
+
|
|
63
|
+
/** A part is in exactly one class; DEFAULT unless the doctrine assigns otherwise. */
|
|
64
|
+
export type RadiusResolutionClass = keyof typeof RADIUS_RESOLUTION_TABLE;
|
|
65
|
+
|
|
66
|
+
export interface RadiusOverrideEntry {
|
|
67
|
+
/** the component name as the doctrine's override table spells it */
|
|
68
|
+
readonly component: string;
|
|
69
|
+
readonly cells: RadiusResolutionRow;
|
|
70
|
+
/** the recorded reason the class would destroy meaning (the DG-RAD-05 bar) */
|
|
71
|
+
readonly why: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* DG-RAD-05 — declared per-component overrides. A row here WINS over the part's
|
|
76
|
+
* resolution class, and sweeps PASS these measurements instead of flagging them.
|
|
77
|
+
*/
|
|
78
|
+
export const RADIUS_OVERRIDE_TABLE: readonly RadiusOverrideEntry[] = [
|
|
79
|
+
{
|
|
80
|
+
component: "Checkbox",
|
|
81
|
+
cells: {
|
|
82
|
+
none: { kind: "px", px: 0 },
|
|
83
|
+
small: { kind: "px", px: 5 },
|
|
84
|
+
medium: { kind: "px", px: 5 },
|
|
85
|
+
large: { kind: "px", px: 5 },
|
|
86
|
+
full: { kind: "px", px: 5 },
|
|
87
|
+
},
|
|
88
|
+
why: "CIRCULAR-AT-FULL kin by shape (20×20), but a circular checkbox reads as a radio — a semantic collision. Clamped at `$2` = 5, never h/2 (R2b DECIDED 2026-08-28: \"NO. Checkbox never becomes full circle.\")",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
component: "Radio disc",
|
|
92
|
+
cells: {
|
|
93
|
+
none: { kind: "half-height" },
|
|
94
|
+
small: { kind: "half-height" },
|
|
95
|
+
medium: { kind: "half-height" },
|
|
96
|
+
large: { kind: "half-height" },
|
|
97
|
+
full: { kind: "half-height" },
|
|
98
|
+
},
|
|
99
|
+
why: "R2b inverted: a square radio reads as a checkbox — the identical collision, mirrored. The disc's circle IS its meaning, so it stays round at every stop including `none` (R2c DECIDED 2026-08-28: always round — identity)",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The resolver's layering, as doctrine spells it: a declared knob-immunity
|
|
105
|
+
* (R-PILL / R-IDENTITY, LC-60/D-09) wins, then a declared override, then the class.
|
|
106
|
+
*/
|
|
107
|
+
export const RADIUS_RESOLUTION_COMPOSITION = ["immunity", "override", "class"] as const;
|
|
108
|
+
|
|
109
|
+
export interface OpenRadiusAdjudication {
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly text: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Adjudications still open (MPO-56 AC-6): sweeps must EXCLUDE these parts
|
|
116
|
+
* from cell assertions and REPORT the exclusion — never silently skip it.
|
|
117
|
+
* An empty list is the closed state (R2 leftovers closed by RULINGS-2026-08-28).
|
|
118
|
+
*/
|
|
119
|
+
export const OPEN_RADIUS_ADJUDICATIONS: readonly OpenRadiusAdjudication[] = [
|
|
120
|
+
|
|
121
|
+
];
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Size-recipe INPUTS (LC-
|
|
2
|
+
* Size-recipe INPUTS (LC-80 RECIPE-INPUTS).
|
|
3
3
|
*
|
|
4
|
-
* Stored numbers are the source of truth.
|
|
5
|
-
* box/type/icon variants, touch heights) are
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Stored numbers are the source of truth. Derived projections (heights,
|
|
5
|
+
* recipes, box/type/icon variants, touch heights, radii) are never
|
|
6
|
+
* handwritten at a call site. The DEFAULT ladder is the measured Tamagui
|
|
7
|
+
* v5 table (`tokenTable`): padX / font / icon / radius per size token,
|
|
8
|
+
* gap == radius at every stop. A linear ratio is only the fallback when
|
|
9
|
+
* a theme sets `{ baseHeight }` and drops the table — it is not the
|
|
10
|
+
* default ladder and is not a signed-off divergence (MPO-46 / R6).
|
|
11
|
+
*
|
|
12
|
+
* Radius steps with the size token. The borderRadius knob is an override
|
|
13
|
+
* that retargets gap through `applyRadiusStop` (gap == radius at every
|
|
14
|
+
* knob stop too). `radiusPx` on inputs is the $4 / medium default (9).
|
|
8
15
|
*
|
|
9
16
|
* Tamagui `createThemes` / theme-builder still emits COLOR palettes only.
|
|
10
17
|
* Do not stuff recipe keys into `getTheme()` / `themes`.
|
|
11
|
-
*
|
|
12
|
-
* DEFAULT uses `tokenHeights` pinned to the stock Tamagui `$size` scale as
|
|
13
|
-
* measured on tamagui.dev ($4=44, $3=36, $5=52, $6=64 — MPO-13 C1/C2: the
|
|
14
|
-
* recipe key and the raw token now agree). `{ baseHeight: 40 }` (no
|
|
15
|
-
* tokenHeights / sizeFactors) regenerates geometrically from
|
|
16
|
-
* `baseHeight * stepRatio ** step`.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import {
|
|
@@ -50,6 +51,31 @@ export type SizeRecipeRatios = {
|
|
|
50
51
|
gap: number;
|
|
51
52
|
};
|
|
52
53
|
|
|
54
|
+
/** Per-token channels on the measured ladder. Gap is not stored — it equals `radius`. */
|
|
55
|
+
export type SizeRecipeTokenChannels = {
|
|
56
|
+
paddingHorizontal: number;
|
|
57
|
+
fontSize: number;
|
|
58
|
+
iconSize: number;
|
|
59
|
+
radius: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type SizeRecipeTokenTable = Partial<Record<SizeRecipeToken, SizeRecipeTokenChannels>>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* borderRadius knob stops (px). Mirrors the v5 radius tokens the knob
|
|
66
|
+
* already resolves ($0/$2/$4/$6/$12 = 0/5/9/16/50). `applyRadiusStop`
|
|
67
|
+
* retargets recipe.gap to the same number so gap == radius at every stop.
|
|
68
|
+
*/
|
|
69
|
+
export const SIZE_RECIPE_RADIUS_STOPS = {
|
|
70
|
+
none: 0,
|
|
71
|
+
small: 5,
|
|
72
|
+
medium: 9,
|
|
73
|
+
large: 16,
|
|
74
|
+
full: 50,
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
export type SizeRecipeRadiusStop = keyof typeof SIZE_RECIPE_RADIUS_STOPS;
|
|
78
|
+
|
|
53
79
|
export type SizeRecipeInputs = {
|
|
54
80
|
baseHeight: number;
|
|
55
81
|
stepRatio: number;
|
|
@@ -61,6 +87,21 @@ export type SizeRecipeInputs = {
|
|
|
61
87
|
* `tokenHeights` is omitted. Factors are `stepRatio ** exponent` otherwise.
|
|
62
88
|
*/
|
|
63
89
|
sizeFactors?: Partial<Record<SizeRecipeToken, number>>;
|
|
90
|
+
/**
|
|
91
|
+
* Measured per-token channels (padX / font / icon / radius). When
|
|
92
|
+
* present, enumerable recipes read this table and set gap = radius.
|
|
93
|
+
* `{ baseHeight }` without an explicit tokenTable drops it so the
|
|
94
|
+
* geometric fallback can run.
|
|
95
|
+
*/
|
|
96
|
+
tokenTable?: SizeRecipeTokenTable;
|
|
97
|
+
/** Same-height tighter family (Chip / `recipeFamily="controlCompact"`). */
|
|
98
|
+
compactTokenTable?: SizeRecipeTokenTable;
|
|
99
|
+
/**
|
|
100
|
+
* Input padX (one space half-step below the button at the same token).
|
|
101
|
+
* $4 = 16 vs the button's 18. Stored on inputs so the generator remains
|
|
102
|
+
* the single writer; Input consumes this, not a second handwritten map.
|
|
103
|
+
*/
|
|
104
|
+
inputPadX?: Partial<Record<SizeRecipeToken, number>>;
|
|
64
105
|
ratios: SizeRecipeRatios;
|
|
65
106
|
compactRatios: SizeRecipeRatios;
|
|
66
107
|
radiusPx: number;
|
|
@@ -103,6 +144,8 @@ export type SizeRecipeTables = {
|
|
|
103
144
|
typeVariants: Record<string, SizeRecipeTypeVariant>;
|
|
104
145
|
iconVariants: Record<string, SizeRecipeIconVariant>;
|
|
105
146
|
heightVariants: Record<string, { height: number }>;
|
|
147
|
+
/** Size-stepped radius (px). Gap on the matching recipe equals this value. */
|
|
148
|
+
radii: Record<string, number>;
|
|
106
149
|
};
|
|
107
150
|
|
|
108
151
|
export type SizeRecipeFamily = SizeRecipeTables & { name: SizeRecipeFamilyName };
|
|
@@ -118,6 +161,7 @@ export type GeneratedSizeRecipes = {
|
|
|
118
161
|
boxVariants: Record<string, SizeRecipeBoxVariant>;
|
|
119
162
|
typeVariants: Record<string, SizeRecipeTypeVariant>;
|
|
120
163
|
iconVariants: Record<string, SizeRecipeIconVariant>;
|
|
164
|
+
radii: Record<string, number>;
|
|
121
165
|
families: RecipeFamilies;
|
|
122
166
|
/** Tile-size ladder (MPO-31) — derived from `inputs.tile` over the measured defaults. */
|
|
123
167
|
tiles: TileSizeLadder;
|
|
@@ -168,40 +212,100 @@ export const defaultSizeFactors: Record<SizeRecipeToken, number> = {
|
|
|
168
212
|
$6: 64 / 44,
|
|
169
213
|
};
|
|
170
214
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Measured Tamagui v5 Button ladder (rendered reference §6.1) plus the
|
|
217
|
+
* integer token-scale fills for stops the site did not photograph.
|
|
218
|
+
*
|
|
219
|
+
* padX = `$space` at the same key (2/4/7/10/13/16/18/24/32).
|
|
220
|
+
* radius = `$radius` at the same key (3/5/7/9/10/16); half-steps sit
|
|
221
|
+
* halfway on that scale (4/6/8) because v5 has no $1.5/$2.5/$3.5 radius.
|
|
222
|
+
* font = Inter body at the same key (11/12/13/14/16/18); half-steps take
|
|
223
|
+
* the next integer stop.
|
|
224
|
+
* icon steps with size and keeps the existing $4 = 16 contract.
|
|
225
|
+
* $2/$3/$4/$6 padX / font / radius are the photographed values (7/13/18/32,
|
|
226
|
+
* 12/13/14/18, 5/7/9/16).
|
|
227
|
+
*/
|
|
228
|
+
export const TAMAGUI_CONTROL_SIZE_TABLE: Record<SizeRecipeToken, SizeRecipeTokenChannels> = {
|
|
229
|
+
$1: { paddingHorizontal: 2, fontSize: 11, iconSize: 10, radius: 3 },
|
|
230
|
+
"$1.5": { paddingHorizontal: 4, fontSize: 12, iconSize: 11, radius: 4 },
|
|
231
|
+
$2: { paddingHorizontal: 7, fontSize: 12, iconSize: 12, radius: 5 },
|
|
232
|
+
"$2.5": { paddingHorizontal: 10, fontSize: 13, iconSize: 14, radius: 6 },
|
|
233
|
+
$3: { paddingHorizontal: 13, fontSize: 13, iconSize: 14, radius: 7 },
|
|
234
|
+
"$3.5": { paddingHorizontal: 16, fontSize: 14, iconSize: 15, radius: 8 },
|
|
235
|
+
$4: { paddingHorizontal: 18, fontSize: 14, iconSize: 16, radius: 9 },
|
|
236
|
+
$true: { paddingHorizontal: 18, fontSize: 14, iconSize: 16, radius: 9 },
|
|
237
|
+
$5: { paddingHorizontal: 24, fontSize: 16, iconSize: 18, radius: 10 },
|
|
238
|
+
$6: { paddingHorizontal: 32, fontSize: 18, iconSize: 20, radius: 16 },
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Input padX — `getSpace(val, { shift: -1 })` on the v5 space scale
|
|
243
|
+
* (rendered reference §6.2). $2/$3/$4 are the photographed 4/10/16;
|
|
244
|
+
* $4 is 16 against the button's 18.
|
|
245
|
+
*/
|
|
246
|
+
export const TAMAGUI_INPUT_PADX: Record<SizeRecipeToken, number> = {
|
|
247
|
+
$1: 1,
|
|
248
|
+
"$1.5": 2,
|
|
249
|
+
$2: 4,
|
|
250
|
+
"$2.5": 7,
|
|
251
|
+
$3: 10,
|
|
252
|
+
"$3.5": 13,
|
|
253
|
+
$4: 16,
|
|
254
|
+
$true: 16,
|
|
255
|
+
$5: 21,
|
|
256
|
+
$6: 24,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* controlCompact: same height / radius (so gap == radius) as control;
|
|
261
|
+
* padX is the Input table (one space half-step tighter); font/icon sit
|
|
262
|
+
* one type step below the control table.
|
|
263
|
+
*/
|
|
264
|
+
export const TAMAGUI_COMPACT_SIZE_TABLE: Record<SizeRecipeToken, SizeRecipeTokenChannels> = {
|
|
265
|
+
$1: { paddingHorizontal: 1, fontSize: 10, iconSize: 9, radius: 3 },
|
|
266
|
+
"$1.5": { paddingHorizontal: 2, fontSize: 11, iconSize: 10, radius: 4 },
|
|
267
|
+
$2: { paddingHorizontal: 4, fontSize: 11, iconSize: 11, radius: 5 },
|
|
268
|
+
"$2.5": { paddingHorizontal: 7, fontSize: 12, iconSize: 12, radius: 6 },
|
|
269
|
+
$3: { paddingHorizontal: 10, fontSize: 12, iconSize: 13, radius: 7 },
|
|
270
|
+
"$3.5": { paddingHorizontal: 13, fontSize: 13, iconSize: 14, radius: 8 },
|
|
271
|
+
$4: { paddingHorizontal: 16, fontSize: 13, iconSize: 14, radius: 9 },
|
|
272
|
+
$true: { paddingHorizontal: 16, fontSize: 13, iconSize: 14, radius: 9 },
|
|
273
|
+
$5: { paddingHorizontal: 21, fontSize: 14, iconSize: 16, radius: 10 },
|
|
274
|
+
$6: { paddingHorizontal: 24, fontSize: 16, iconSize: 18, radius: 16 },
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Geometric fallback only — used when `{ baseHeight }` drops tokenTable.
|
|
278
|
+
// Anchored at the measured $4 so a doubled baseHeight still doubles $4.
|
|
279
|
+
const FALLBACK_RATIOS: SizeRecipeRatios = {
|
|
176
280
|
padX: 18 / 44,
|
|
177
281
|
font: 14 / 44,
|
|
178
282
|
icon: 16 / 44,
|
|
179
283
|
gap: 9 / 44,
|
|
180
284
|
};
|
|
181
285
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// ($space.2).
|
|
185
|
-
const DEFAULT_COMPACT_RATIOS: SizeRecipeRatios = {
|
|
186
|
-
padX: 13 / 44,
|
|
286
|
+
const FALLBACK_COMPACT_RATIOS: SizeRecipeRatios = {
|
|
287
|
+
padX: 16 / 44,
|
|
187
288
|
font: 13 / 44,
|
|
188
289
|
icon: 14 / 44,
|
|
189
|
-
gap:
|
|
290
|
+
gap: 9 / 44,
|
|
190
291
|
};
|
|
191
292
|
|
|
192
293
|
export const DEFAULT_SIZE_RECIPE_INPUTS: SizeRecipeInputs = {
|
|
193
294
|
// Tamagui `$size.true` = 44 — the iOS touch minimum, measured on every
|
|
194
|
-
// tamagui.dev button and input (MPO-13 C2
|
|
295
|
+
// tamagui.dev button and input (MPO-13 C2).
|
|
195
296
|
baseHeight: 44,
|
|
196
297
|
stepRatio: 1.25,
|
|
197
298
|
tokenSteps: { ...SIZE_TOKEN_EXPONENTS },
|
|
198
299
|
tokenHeights: { ...DEFAULT_TOKEN_HEIGHTS },
|
|
199
300
|
sizeFactors: { ...defaultSizeFactors },
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
301
|
+
tokenTable: { ...TAMAGUI_CONTROL_SIZE_TABLE },
|
|
302
|
+
compactTokenTable: { ...TAMAGUI_COMPACT_SIZE_TABLE },
|
|
303
|
+
inputPadX: { ...TAMAGUI_INPUT_PADX },
|
|
304
|
+
ratios: { ...FALLBACK_RATIOS },
|
|
305
|
+
compactRatios: { ...FALLBACK_COMPACT_RATIOS },
|
|
306
|
+
// Tamagui `$radius.true` = 9 — the $4 / medium default. Per-token
|
|
307
|
+
// radii live on tokenTable; this is the knob's medium stop and the
|
|
308
|
+
// $4 table row, not a flat radius for every size.
|
|
205
309
|
radiusPx: 9,
|
|
206
310
|
touchFloor: 44,
|
|
207
311
|
};
|
|
@@ -212,12 +316,31 @@ export const TOUCH_HEIGHT_FLOOR = DEFAULT_SIZE_RECIPE_INPUTS.touchFloor;
|
|
|
212
316
|
|
|
213
317
|
const MD_TOUCH_MIN = 48;
|
|
214
318
|
|
|
319
|
+
function cloneTokenTable(table?: SizeRecipeTokenTable): SizeRecipeTokenTable | undefined {
|
|
320
|
+
if (!table) return undefined;
|
|
321
|
+
const next: SizeRecipeTokenTable = {};
|
|
322
|
+
for (const token of SIZE_RECIPE_TOKENS) {
|
|
323
|
+
const row = table[token];
|
|
324
|
+
if (row) next[token] = { ...row };
|
|
325
|
+
}
|
|
326
|
+
return next;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function clonePadX(
|
|
330
|
+
table?: Partial<Record<SizeRecipeToken, number>>,
|
|
331
|
+
): Partial<Record<SizeRecipeToken, number>> | undefined {
|
|
332
|
+
return table ? { ...table } : undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
215
335
|
export function cloneSizeRecipeInputs(inputs: SizeRecipeInputs): SizeRecipeInputs {
|
|
216
336
|
return {
|
|
217
337
|
...inputs,
|
|
218
338
|
tokenSteps: { ...inputs.tokenSteps },
|
|
219
339
|
tokenHeights: inputs.tokenHeights ? { ...inputs.tokenHeights } : undefined,
|
|
220
340
|
sizeFactors: inputs.sizeFactors ? { ...inputs.sizeFactors } : undefined,
|
|
341
|
+
tokenTable: cloneTokenTable(inputs.tokenTable),
|
|
342
|
+
compactTokenTable: cloneTokenTable(inputs.compactTokenTable),
|
|
343
|
+
inputPadX: clonePadX(inputs.inputPadX),
|
|
221
344
|
ratios: { ...inputs.ratios },
|
|
222
345
|
compactRatios: { ...inputs.compactRatios },
|
|
223
346
|
tile: inputs.tile ? cloneTileSizeInputs(inputs.tile) : undefined,
|
|
@@ -225,14 +348,17 @@ export function cloneSizeRecipeInputs(inputs: SizeRecipeInputs): SizeRecipeInput
|
|
|
225
348
|
}
|
|
226
349
|
|
|
227
350
|
/**
|
|
228
|
-
* Fill defaults. `{ baseHeight: 36 }` drops handwritten tokenHeights/
|
|
229
|
-
* so every token regenerates from stepRatio.
|
|
351
|
+
* Fill defaults. `{ baseHeight: 36 }` drops handwritten tokenHeights /
|
|
352
|
+
* sizeFactors / token tables so every token regenerates from stepRatio.
|
|
230
353
|
*/
|
|
231
354
|
export function resolveSizeRecipeInputs(partial: Partial<SizeRecipeInputs> = {}): SizeRecipeInputs {
|
|
232
355
|
const scalingOverride =
|
|
233
356
|
partial.baseHeight != null || partial.stepRatio != null || partial.tokenSteps != null;
|
|
234
357
|
const hasTokenHeights = Object.prototype.hasOwnProperty.call(partial, "tokenHeights");
|
|
235
358
|
const hasSizeFactors = Object.prototype.hasOwnProperty.call(partial, "sizeFactors");
|
|
359
|
+
const hasTokenTable = Object.prototype.hasOwnProperty.call(partial, "tokenTable");
|
|
360
|
+
const hasCompactTable = Object.prototype.hasOwnProperty.call(partial, "compactTokenTable");
|
|
361
|
+
const hasInputPadX = Object.prototype.hasOwnProperty.call(partial, "inputPadX");
|
|
236
362
|
|
|
237
363
|
const resolved: SizeRecipeInputs = {
|
|
238
364
|
baseHeight: partial.baseHeight ?? DEFAULT_SIZE_RECIPE_INPUTS.baseHeight,
|
|
@@ -260,6 +386,26 @@ export function resolveSizeRecipeInputs(partial: Partial<SizeRecipeInputs> = {})
|
|
|
260
386
|
resolved.sizeFactors = { ...DEFAULT_SIZE_RECIPE_INPUTS.sizeFactors };
|
|
261
387
|
}
|
|
262
388
|
|
|
389
|
+
if (hasTokenTable) {
|
|
390
|
+
resolved.tokenTable = partial.tokenTable ? cloneTokenTable(partial.tokenTable) : undefined;
|
|
391
|
+
} else if (!scalingOverride) {
|
|
392
|
+
resolved.tokenTable = cloneTokenTable(DEFAULT_SIZE_RECIPE_INPUTS.tokenTable);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (hasCompactTable) {
|
|
396
|
+
resolved.compactTokenTable = partial.compactTokenTable
|
|
397
|
+
? cloneTokenTable(partial.compactTokenTable)
|
|
398
|
+
: undefined;
|
|
399
|
+
} else if (!scalingOverride) {
|
|
400
|
+
resolved.compactTokenTable = cloneTokenTable(DEFAULT_SIZE_RECIPE_INPUTS.compactTokenTable);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (hasInputPadX) {
|
|
404
|
+
resolved.inputPadX = partial.inputPadX ? clonePadX(partial.inputPadX) : undefined;
|
|
405
|
+
} else if (!scalingOverride) {
|
|
406
|
+
resolved.inputPadX = clonePadX(DEFAULT_SIZE_RECIPE_INPUTS.inputPadX);
|
|
407
|
+
}
|
|
408
|
+
|
|
263
409
|
return resolved;
|
|
264
410
|
}
|
|
265
411
|
|
|
@@ -282,15 +428,37 @@ export function sizeRecipeFromHeight(
|
|
|
282
428
|
height: number,
|
|
283
429
|
ratios: SizeRecipeRatios = DEFAULT_SIZE_RECIPE_INPUTS.ratios,
|
|
284
430
|
): SizeRecipe {
|
|
431
|
+
const gap = Math.round(height * ratios.gap);
|
|
285
432
|
return {
|
|
286
433
|
height,
|
|
287
434
|
paddingHorizontal: Math.round(height * ratios.padX),
|
|
288
435
|
fontSize: Math.round(height * ratios.font),
|
|
289
436
|
iconSize: Math.round(height * ratios.icon),
|
|
290
|
-
gap
|
|
437
|
+
gap,
|
|
291
438
|
};
|
|
292
439
|
}
|
|
293
440
|
|
|
441
|
+
export function recipeFromTokenChannels(
|
|
442
|
+
height: number,
|
|
443
|
+
channels: SizeRecipeTokenChannels,
|
|
444
|
+
): SizeRecipe {
|
|
445
|
+
return {
|
|
446
|
+
height,
|
|
447
|
+
paddingHorizontal: channels.paddingHorizontal,
|
|
448
|
+
fontSize: channels.fontSize,
|
|
449
|
+
iconSize: channels.iconSize,
|
|
450
|
+
gap: channels.radius,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Retarget inner gap to a borderRadius knob stop so gap == that stop's radius. */
|
|
455
|
+
export function applyRadiusStop<T extends { gap: number }>(
|
|
456
|
+
recipe: T,
|
|
457
|
+
stop: SizeRecipeRadiusStop,
|
|
458
|
+
): T {
|
|
459
|
+
return { ...recipe, gap: SIZE_RECIPE_RADIUS_STOPS[stop] };
|
|
460
|
+
}
|
|
461
|
+
|
|
294
462
|
function generateTouchHeights(
|
|
295
463
|
heights: Record<string, number>,
|
|
296
464
|
inputs: SizeRecipeInputs,
|
|
@@ -326,9 +494,18 @@ function generateTouchHeights(
|
|
|
326
494
|
return heightsTouch;
|
|
327
495
|
}
|
|
328
496
|
|
|
497
|
+
function channelsForToken(
|
|
498
|
+
token: string,
|
|
499
|
+
table: SizeRecipeTokenTable | undefined,
|
|
500
|
+
): SizeRecipeTokenChannels | undefined {
|
|
501
|
+
if (!table) return undefined;
|
|
502
|
+
return table[token as SizeRecipeToken] ?? (token === "$true" ? table.$4 : undefined);
|
|
503
|
+
}
|
|
504
|
+
|
|
329
505
|
export function generateSizeRecipeTables(
|
|
330
506
|
inputs: SizeRecipeInputs,
|
|
331
507
|
ratios: SizeRecipeRatios = inputs.ratios,
|
|
508
|
+
table: SizeRecipeTokenTable | undefined = inputs.tokenTable,
|
|
332
509
|
): SizeRecipeTables {
|
|
333
510
|
const tokens = SIZE_RECIPE_TOKENS;
|
|
334
511
|
const heights: Record<string, number> = {};
|
|
@@ -346,11 +523,17 @@ export function generateSizeRecipeTables(
|
|
|
346
523
|
const typeVariants: Record<string, SizeRecipeTypeVariant> = {};
|
|
347
524
|
const iconVariants: Record<string, SizeRecipeIconVariant> = {};
|
|
348
525
|
const heightVariants: Record<string, { height: number }> = {};
|
|
526
|
+
const radii: Record<string, number> = {};
|
|
349
527
|
|
|
350
528
|
for (const token of tokens) {
|
|
351
|
-
const
|
|
529
|
+
const channels = channelsForToken(token, table);
|
|
530
|
+
const recipe = channels
|
|
531
|
+
? recipeFromTokenChannels(heights[token], channels)
|
|
532
|
+
: sizeRecipeFromHeight(heights[token], ratios);
|
|
533
|
+
const radius = channels ? channels.radius : recipe.gap;
|
|
352
534
|
recipes[token] = recipe;
|
|
353
535
|
recipesTouch[token] = sizeRecipeFromHeight(touchHeights[token], ratios);
|
|
536
|
+
radii[token] = radius;
|
|
354
537
|
boxVariants[token] = {
|
|
355
538
|
height: recipe.height,
|
|
356
539
|
paddingHorizontal: recipe.paddingHorizontal,
|
|
@@ -372,6 +555,7 @@ export function generateSizeRecipeTables(
|
|
|
372
555
|
typeVariants,
|
|
373
556
|
iconVariants,
|
|
374
557
|
heightVariants,
|
|
558
|
+
radii,
|
|
375
559
|
};
|
|
376
560
|
}
|
|
377
561
|
|
|
@@ -379,10 +563,12 @@ export function createRecipeFamilies(
|
|
|
379
563
|
inputs: SizeRecipeInputs | Partial<SizeRecipeInputs> = {},
|
|
380
564
|
): RecipeFamilies {
|
|
381
565
|
const resolved = cloneSizeRecipeInputs(resolveSizeRecipeInputs(inputs));
|
|
382
|
-
const control = generateSizeRecipeTables(resolved, resolved.ratios);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
566
|
+
const control = generateSizeRecipeTables(resolved, resolved.ratios, resolved.tokenTable);
|
|
567
|
+
const controlCompact = generateSizeRecipeTables(
|
|
568
|
+
resolved,
|
|
569
|
+
resolved.compactRatios,
|
|
570
|
+
resolved.compactTokenTable,
|
|
571
|
+
);
|
|
386
572
|
return {
|
|
387
573
|
control: { ...control, name: "control" },
|
|
388
574
|
controlCompact: { ...controlCompact, name: "controlCompact" },
|
|
@@ -402,6 +588,7 @@ export function generateSizeRecipes(partial: Partial<SizeRecipeInputs> = {}): Ge
|
|
|
402
588
|
boxVariants: control.boxVariants,
|
|
403
589
|
typeVariants: control.typeVariants,
|
|
404
590
|
iconVariants: control.iconVariants,
|
|
591
|
+
radii: control.radii,
|
|
405
592
|
families,
|
|
406
593
|
tiles: generateTileSizeLadder(resolveTileSizeInputs(inputs.tile ?? {})),
|
|
407
594
|
};
|
package/src/theme/recipes.ts
CHANGED
|
@@ -23,9 +23,30 @@ export interface TextStateProps {
|
|
|
23
23
|
fontSize?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export interface
|
|
26
|
+
export interface ElevationPaintProps {
|
|
27
|
+
shadowColor?: string;
|
|
28
|
+
shadowOffset?: { width: number; height: number };
|
|
29
|
+
shadowOpacity?: number;
|
|
30
|
+
shadowRadius?: number;
|
|
31
|
+
/** Web: raw boxShadow so paint does not depend on Tamagui's elevation variant. */
|
|
32
|
+
style?: { boxShadow: string };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ElevationStateProps extends ElevationPaintProps {
|
|
27
36
|
/** Size token on web ("$1"); numeric tokens.size value on native (RN-safe). */
|
|
28
37
|
elevation?: string | number;
|
|
38
|
+
/** Dark-scheme surface tint (existing $color ramp). Light leaves this unset. */
|
|
39
|
+
backgroundColor?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Complete elevation chrome to spread onto a raised frame (E-CONTROL wrappers,
|
|
44
|
+
* InputParts Box). Shadows + Android dp. No fill — filled inputs read
|
|
45
|
+
* `inputBackground`; overlays read `elevatedSurface.backgroundColor`.
|
|
46
|
+
*/
|
|
47
|
+
export interface ElevationChromeProps extends ElevationPaintProps {
|
|
48
|
+
/** Android dp (2/4/8), not the Tamagui size token. */
|
|
49
|
+
elevation?: number;
|
|
29
50
|
}
|
|
30
51
|
|
|
31
52
|
// ── Disabled state recipe (LC-40 DISABLED-VISIBLE) ───────────────────────
|
|
@@ -102,8 +123,8 @@ export interface KnobRecipe<T> {
|
|
|
102
123
|
* Corner smoothing carrier (Axiom 15 OPTICS): radius-bearing fragments gain
|
|
103
124
|
* `className: "mp-corner-smooth"` when `cornerSmoothing: "smooth"` (web
|
|
104
125
|
* only) — the mounted stylesheet in `cornerSmoothing.ts` resolves it to
|
|
105
|
-
* `corner-shape: squircle
|
|
106
|
-
*
|
|
126
|
+
* `corner-shape: squircle` (computed `superellipse(2)` on Chromium 152).
|
|
127
|
+
* Engines without the property drop the declaration; native is absent.
|
|
107
128
|
*/
|
|
108
129
|
export interface CornerSmoothingCarrier {
|
|
109
130
|
className?: string;
|
|
@@ -115,7 +136,36 @@ export interface KnobProps {
|
|
|
115
136
|
borderColor: string;
|
|
116
137
|
borderWidth: number;
|
|
117
138
|
} & CornerSmoothingCarrier;
|
|
139
|
+
/**
|
|
140
|
+
* The radius for chrome NESTED INSIDE a radius-bearing frame — a tab
|
|
141
|
+
* indicator inside a tab bar, a code block inside a card, a toast's action
|
|
142
|
+
* strip. It resolves one or more steps SMALLER than `borderRadius` at every
|
|
143
|
+
* knob stop (medium: `$3` inside `$4`, full: `$5` inside `$12`), because a
|
|
144
|
+
* child corner that matched its parent's would read as a second frame.
|
|
145
|
+
*
|
|
146
|
+
* MPO-19 X12 (00 L9c): this was `borderRadiusOuter`, which named the
|
|
147
|
+
* opposite of what it resolves to — every one of its consumers uses it for
|
|
148
|
+
* inner chrome, so the name told each of them the wrong story. Renamed;
|
|
149
|
+
* `borderRadiusOuter` stays as a deprecated alias for one release.
|
|
150
|
+
*/
|
|
151
|
+
borderRadiusNested: { borderRadius: string } & CornerSmoothingCarrier;
|
|
152
|
+
/** @deprecated MPO-19 X12 — use `borderRadiusNested`. Removed after one release. */
|
|
118
153
|
borderRadiusOuter: { borderRadius: string } & CornerSmoothingCarrier;
|
|
154
|
+
/**
|
|
155
|
+
* A FILLED control's border. Same width as the outlined one, so switching
|
|
156
|
+
* fill style never moves a layout by 2px, but `transparent` — which is what
|
|
157
|
+
* Tamagui paints and what the fill style means: the surface is the
|
|
158
|
+
* affordance, so a ring around it is a second one.
|
|
159
|
+
*
|
|
160
|
+
* MPO-19 X3: filled controls spread the `borderRadius` fragment, whose
|
|
161
|
+
* `borderColor` is `$borderColor`, so with the default `borderWidth: medium`
|
|
162
|
+
* every default Button carried a visible 1px ring that stock Tamagui does
|
|
163
|
+
* not draw. The width still resolves through the `borderWidth` knob — set it
|
|
164
|
+
* to `none` and both fragments go to 0.
|
|
165
|
+
*/
|
|
166
|
+
controlBorderFilled: { borderColor: string; borderWidth: number };
|
|
167
|
+
/** An OUTLINED control's border: the outline IS the affordance, so it is at least a hairline. */
|
|
168
|
+
controlBorderOutlined: { borderColor: string; borderWidth: number };
|
|
119
169
|
/**
|
|
120
170
|
* Axiom 1 child-clip cap for padded containers (Card/Panel/DialogContent):
|
|
121
171
|
* the radius token while it fits, or the padding px once it would exceed
|
|
@@ -124,6 +174,19 @@ export interface KnobProps {
|
|
|
124
174
|
containerRadius: { borderRadius: string | number } & CornerSmoothingCarrier;
|
|
125
175
|
/** Size token on web ("$1"); numeric tokens.size value on native (RN-safe). */
|
|
126
176
|
elevation: string | number | undefined;
|
|
177
|
+
/**
|
|
178
|
+
* Overlay elevation token (dialogs, popovers, Panel). Distinct from
|
|
179
|
+
* `elevation` so an overlay over a scrim is not the same $1 stop every
|
|
180
|
+
* Button carries. Spread via `elevatedSurface`; do not reuse `elevation`.
|
|
181
|
+
*/
|
|
182
|
+
overlayElevation: string | number | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* Complete shadow fragment for E-CONTROL / wrapper Views. Spread whole.
|
|
185
|
+
* Empty when `elevation=none`. Android `elevation` here is dp (2/4/8).
|
|
186
|
+
*/
|
|
187
|
+
elevationChrome: ElevationChromeProps;
|
|
188
|
+
/** Resolved scheme the fragments were built for (LC-85). */
|
|
189
|
+
scheme: "light" | "dark";
|
|
127
190
|
surface: { backgroundColor: string; borderColor: string; borderWidth: number };
|
|
128
191
|
/** Content tier (Bento content card): padded frame, no border, no shadow. Radius carries the container cap. */
|
|
129
192
|
cardSurface: {
|
|
@@ -138,7 +201,8 @@ export interface KnobProps {
|
|
|
138
201
|
borderWidth: number;
|
|
139
202
|
borderRadius: string;
|
|
140
203
|
elevation: string | number | undefined;
|
|
141
|
-
} &
|
|
204
|
+
} & ElevationPaintProps &
|
|
205
|
+
CornerSmoothingCarrier;
|
|
142
206
|
/** Feature/marketing tier (auth, hero cards): big radius + ultra-soft wide shadow. */
|
|
143
207
|
featureSurface: {
|
|
144
208
|
backgroundColor: string;
|