@multiplatform.one/theme 7.2.2 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/theme/Surface.spec.tsx +1 -1
- package/src/theme/createDefaultThemeConfig.spec.ts +5 -5
- package/src/theme/index.ts +1 -0
- package/src/theme/layoutTokens.spec.ts +3 -2
- package/src/theme/layoutTokens.ts +22 -10
- package/src/theme/mpo13Proof.spec.ts +149 -0
- package/src/theme/recipeInputs.ts +70 -30
- package/src/theme/resolveKnobs.spec.ts +5 -5
- package/src/theme/sizeRecipes.spec.ts +44 -42
- package/src/theme/sizeRecipes.ts +19 -5
- package/src/theme/tileSizes.spec.ts +139 -0
- package/src/theme/tileSizes.ts +291 -0
- package/types/theme/index.d.ts +1 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +22 -10
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/recipeInputs.d.ts +14 -4
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +9 -1
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/tileSizes.d.ts +139 -0
- package/types/theme/tileSizes.d.ts.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@tamagui/toast": "2.7.6",
|
|
54
54
|
"@tamagui/web": "2.7.6",
|
|
55
55
|
"react-cookie": "^8.1.2",
|
|
56
|
-
"@multiplatform.one/platform": "7.
|
|
57
|
-
"@multiplatform.one/store": "7.
|
|
56
|
+
"@multiplatform.one/platform": "7.4.0",
|
|
57
|
+
"@multiplatform.one/store": "7.4.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@tamagui/animations-css": "2.7.6",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"tamagui": "2.7.6",
|
|
70
70
|
"typescript": "~5.9.3",
|
|
71
71
|
"vitest": "^4.1.5",
|
|
72
|
-
"@multiplatform.one/config": "7.
|
|
73
|
-
"@multiplatform.one/test-utils": "7.
|
|
72
|
+
"@multiplatform.one/config": "7.4.0",
|
|
73
|
+
"@multiplatform.one/test-utils": "7.4.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@tamagui/animations-css": "^2.0.0-rc",
|
|
@@ -117,7 +117,7 @@ describe("useResolvedKnobs inside Surface", () => {
|
|
|
117
117
|
expect(result.current.knobProps.sizeToken).toBe("$3");
|
|
118
118
|
expect(result.current.knobProps.density).toBe("compact");
|
|
119
119
|
expect(result.current.knobProps.gap).toEqual({ gap: "$2" });
|
|
120
|
-
expect(result.current.knobProps.control.height).toBe(
|
|
120
|
+
expect(result.current.knobProps.control.height).toBe(36);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it("a large size request inside a medium Surface resolves medium", () => {
|
|
@@ -124,10 +124,10 @@ describe("createDefaultThemeConfig — builderOptions.getTheme composition (DF-0
|
|
|
124
124
|
expect(config.recipeFamilies?.control.boxVariants.$4.height).toBe(40);
|
|
125
125
|
expect(config.recipeFamilies?.control.recipes.$4).toEqual({
|
|
126
126
|
height: 40,
|
|
127
|
-
paddingHorizontal: Math.round(40 * (
|
|
128
|
-
fontSize: Math.round(40 * (14 /
|
|
129
|
-
iconSize: Math.round(40 * (16 /
|
|
130
|
-
gap: Math.round(40 * (
|
|
127
|
+
paddingHorizontal: Math.round(40 * (18 / 44)),
|
|
128
|
+
fontSize: Math.round(40 * (14 / 44)),
|
|
129
|
+
iconSize: Math.round(40 * (16 / 44)),
|
|
130
|
+
gap: Math.round(40 * (9 / 44)),
|
|
131
131
|
});
|
|
132
132
|
expect(config.recipeInputs?.baseHeight).toBe(40);
|
|
133
133
|
const themes = (config.tamagui as unknown as { themes: RawThemes }).themes;
|
|
@@ -147,6 +147,6 @@ describe("createDefaultThemeConfig — builderOptions.getTheme composition (DF-0
|
|
|
147
147
|
expect(normalizeToHex(theme.outlineColor)).toBeTruthy();
|
|
148
148
|
expect(theme.textMuted).toBe(theme.color11);
|
|
149
149
|
}
|
|
150
|
-
expect(config.recipeFamilies?.control.boxVariants.$4.height).toBe(
|
|
150
|
+
expect(config.recipeFamilies?.control.boxVariants.$4.height).toBe(44);
|
|
151
151
|
});
|
|
152
152
|
});
|
package/src/theme/index.ts
CHANGED
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
} from "./layoutTokens";
|
|
19
19
|
|
|
20
20
|
describe("layoutBreakpoints (DG-LAY-01)", () => {
|
|
21
|
-
it("
|
|
21
|
+
it("pivots overlays at medium 640 (Tamagui $sm — MPO-13 C5); expanded 860 stays the pane class", () => {
|
|
22
22
|
expect(layoutBreakpoints.expanded).toBe(860);
|
|
23
|
-
expect(OVERLAY_BREAKPOINT).toBe(
|
|
23
|
+
expect(OVERLAY_BREAKPOINT).toBe(640);
|
|
24
|
+
expect(OVERLAY_BREAKPOINT).toBe(layoutBreakpoints.medium);
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
it("documents DeskShell narrow as medium 640", () => {
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* Hybrid of shipped MP1 behavior + Material window-size naming:
|
|
11
11
|
* - **Names** follow Material (compact → xl).
|
|
12
12
|
* - **Numbers** prefer already-shipped thresholds:
|
|
13
|
-
* - `medium` @ 640 = Tamagui `$sm` / DeskShell narrow (Fluent sm)
|
|
14
|
-
*
|
|
13
|
+
* - `medium` @ 640 = Tamagui `$sm` / DeskShell narrow (Fluent sm) — and,
|
|
14
|
+
* since MPO-13, the overlay sheet↔float pivot (see OVERLAY_BREAKPOINT).
|
|
15
|
+
* - `expanded` @ 860 = pane-budget class only (~Material 840). No Tamagui
|
|
16
|
+
* counterpart; Tamagui has no pane system, so this stays additive.
|
|
15
17
|
* - `large` / `xl` = Tamagui `$lg` / `$xl`.
|
|
16
18
|
*
|
|
17
19
|
* Fluent (480/640/1024/1366/1920) and Material (600/840/1200/1600) stay in the
|
|
@@ -19,9 +21,9 @@
|
|
|
19
21
|
*
|
|
20
22
|
* | Class | Min px | Max panes (default) | Tamagui media | Notes |
|
|
21
23
|
* |-----------|--------|---------------------|---------------|--------------------------------|
|
|
22
|
-
* | compact | 0 | 1 | < `$sm` | phones; drawer nav
|
|
23
|
-
* | medium | 640 | 1 | `$sm` | DeskShell rail;
|
|
24
|
-
* | expanded | 860 | 2 | — (custom) |
|
|
24
|
+
* | compact | 0 | 1 | < `$sm` | phones; drawer nav; sheets |
|
|
25
|
+
* | medium | 640 | 1 | `$sm` | DeskShell rail; overlays float |
|
|
26
|
+
* | expanded | 860 | 2 | — (custom) | 2 panes |
|
|
25
27
|
* | large | 1024 | 2 | `$lg` | |
|
|
26
28
|
* | xl | 1280 | 2 | `$xl` | |
|
|
27
29
|
*
|
|
@@ -40,9 +42,13 @@ export type LayoutSizeClass = "compact" | "medium" | "expanded" | "large" | "xl"
|
|
|
40
42
|
|
|
41
43
|
/** Named min-widths. Prefer these over literals in layout code. */
|
|
42
44
|
export const layoutBreakpoints = {
|
|
43
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* End of compact / DeskShell narrow drawer threshold (Tamagui `$sm`).
|
|
47
|
+
* Also the overlay sheet↔float pivot and the DataTable card↔grid pivot —
|
|
48
|
+
* one idiom flip per viewport (MPO-13 C5).
|
|
49
|
+
*/
|
|
44
50
|
medium: 640,
|
|
45
|
-
/** Two-pane
|
|
51
|
+
/** Two-pane budget threshold (~M3 expanded 840). Not an overlay pivot. */
|
|
46
52
|
expanded: 860,
|
|
47
53
|
/** Tamagui `$lg`. */
|
|
48
54
|
large: 1024,
|
|
@@ -53,10 +59,16 @@ export const layoutBreakpoints = {
|
|
|
53
59
|
export type LayoutBreakpointName = keyof typeof layoutBreakpoints;
|
|
54
60
|
|
|
55
61
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
62
|
+
* Overlay sheet↔float pivot for FloatingPanel / AdaptivePopup consumers.
|
|
63
|
+
*
|
|
64
|
+
* MPO-13 C5: was `layoutBreakpoints.expanded` (860, a custom threshold with
|
|
65
|
+
* no Tamagui counterpart), which split the system in two — at 700px a
|
|
66
|
+
* DataTable rendered its desktop grid while a Select in one of its cells
|
|
67
|
+
* opened as a bottom sheet. Overlays now pivot where the table does:
|
|
68
|
+
* `layoutBreakpoints.medium` (640, Tamagui v5 `$sm` — the same `media.sm`
|
|
69
|
+
* key DataTableRoot reads), so every surface flips idiom at one width.
|
|
58
70
|
*/
|
|
59
|
-
export const OVERLAY_BREAKPOINT = layoutBreakpoints.
|
|
71
|
+
export const OVERLAY_BREAKPOINT = layoutBreakpoints.medium;
|
|
60
72
|
|
|
61
73
|
/** DeskShell sidebar collapse — same value as `layoutBreakpoints.medium`. */
|
|
62
74
|
export const NARROW_BREAKPOINT = layoutBreakpoints.medium;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-13 PROOF — the measured corrections, asserted against the values read
|
|
3
|
+
* off the live tamagui.dev computed styles (reference doc §4, §5.1, §6.1,
|
|
4
|
+
* §6.2; 1440x900 DPR2, light and dark).
|
|
5
|
+
*
|
|
6
|
+
* This file exists so the corrections cannot silently regress: it pins the
|
|
7
|
+
* generator's OUTPUT to the reference numbers, and it pins the RELATIONSHIPS
|
|
8
|
+
* that produced them (gap tracks radius; space ≈ 40% of size; input padding
|
|
9
|
+
* one space step below button) rather than the literals alone. A design that
|
|
10
|
+
* only looks right at one hardcoded set of numbers fails the acceptance test
|
|
11
|
+
* — every value below resolves through `SizeRecipeInputs`, so re-pointing the
|
|
12
|
+
* knob re-derives all of them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
import { layoutBreakpoints, OVERLAY_BREAKPOINT } from "./layoutTokens";
|
|
17
|
+
import { DEFAULT_SIZE_RECIPE_INPUTS, generateSizeRecipes, sizeRecipeForToken } from "./sizeRecipes";
|
|
18
|
+
|
|
19
|
+
/** Stock Tamagui token scales, read from the live CSS variables (§4). */
|
|
20
|
+
const tamaguiSize = {
|
|
21
|
+
$1: 20,
|
|
22
|
+
"$1.5": 24,
|
|
23
|
+
$2: 28,
|
|
24
|
+
"$2.5": 32,
|
|
25
|
+
$3: 36,
|
|
26
|
+
"$3.5": 40,
|
|
27
|
+
$4: 44,
|
|
28
|
+
$true: 44,
|
|
29
|
+
$5: 52,
|
|
30
|
+
$6: 64,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const tamaguiRadiusTrue = 9;
|
|
34
|
+
const tamaguiSpaceTrue = 18;
|
|
35
|
+
|
|
36
|
+
describe("MPO-13 C1/C2 — $4 means ONE height, and it is 44", () => {
|
|
37
|
+
it("every recipe height equals the raw Tamagui $size token at the same key", () => {
|
|
38
|
+
const { heights } = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
|
|
39
|
+
for (const [token, px] of Object.entries(tamaguiSize)) {
|
|
40
|
+
expect(heights[token], `heights.${token}`).toBe(px);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("baseHeight is $size.true, so recipe and token cannot re-diverge", () => {
|
|
45
|
+
expect(DEFAULT_SIZE_RECIPE_INPUTS.baseHeight).toBe(tamaguiSize.$true);
|
|
46
|
+
expect(sizeRecipeForToken("$4").height).toBe(tamaguiSize.$4);
|
|
47
|
+
expect(sizeRecipeForToken("$true").height).toBe(sizeRecipeForToken("$4").height);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("the $4 control is the measured 44 / 18 / 14 / 16 / 9", () => {
|
|
51
|
+
expect(sizeRecipeForToken("$4")).toEqual({
|
|
52
|
+
height: 44,
|
|
53
|
+
paddingHorizontal: 18,
|
|
54
|
+
fontSize: 14,
|
|
55
|
+
iconSize: 16,
|
|
56
|
+
gap: 9,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("MPO-13 C3 — radius is $radius.true = 9", () => {
|
|
62
|
+
it("radiusPx is the stored input, at the measured value", () => {
|
|
63
|
+
expect(DEFAULT_SIZE_RECIPE_INPUTS.radiusPx).toBe(tamaguiRadiusTrue);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("gap tracks radius exactly at $4 (the reference's stated rule)", () => {
|
|
67
|
+
expect(sizeRecipeForToken("$4").gap).toBe(DEFAULT_SIZE_RECIPE_INPUTS.radiusPx);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("radius never rides the size axis — it stays on the borderRadius knob", () => {
|
|
71
|
+
for (const token of ["$2", "$3", "$4", "$5", "$6"] as const) {
|
|
72
|
+
expect(sizeRecipeForToken(token)).not.toHaveProperty("radius");
|
|
73
|
+
expect(sizeRecipeForToken(token)).not.toHaveProperty("borderRadius");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("MPO-13 — the ratios are relationships, not literals", () => {
|
|
79
|
+
it("padding at $4 is $space.true, ~40% of the size step (reference §4)", () => {
|
|
80
|
+
const recipe = sizeRecipeForToken("$4");
|
|
81
|
+
expect(recipe.paddingHorizontal).toBe(tamaguiSpaceTrue);
|
|
82
|
+
const spaceToSize = recipe.paddingHorizontal / recipe.height;
|
|
83
|
+
expect(spaceToSize).toBeGreaterThan(0.38);
|
|
84
|
+
expect(spaceToSize).toBeLessThan(0.42);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("compact is one space step below control at the SAME height (LC-76)", () => {
|
|
88
|
+
const control = sizeRecipeForToken("$4");
|
|
89
|
+
const compact = sizeRecipeForToken("$4", { family: "controlCompact" });
|
|
90
|
+
expect(compact.height).toBe(control.height);
|
|
91
|
+
expect(compact.paddingHorizontal).toBeLessThan(control.paddingHorizontal);
|
|
92
|
+
// $space.3 = 13 vs $space.true = 18 — the measured Input↔Button offset.
|
|
93
|
+
expect(compact.paddingHorizontal).toBe(13);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("re-pointing baseHeight re-derives every channel — no hardcoded 44", () => {
|
|
97
|
+
const scaled = generateSizeRecipes({ baseHeight: 88 });
|
|
98
|
+
const base = sizeRecipeForToken("$4");
|
|
99
|
+
expect(scaled.recipes.$4.height).toBe(base.height * 2);
|
|
100
|
+
expect(scaled.recipes.$4.paddingHorizontal).toBe(base.paddingHorizontal * 2);
|
|
101
|
+
expect(scaled.recipes.$4.fontSize).toBe(base.fontSize * 2);
|
|
102
|
+
expect(scaled.recipes.$4.gap).toBe(base.gap * 2);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("MPO-13 C4/C5 — one breakpoint system", () => {
|
|
107
|
+
it("overlays pivot on the Tamagui v5 $sm key the table already uses", () => {
|
|
108
|
+
expect(OVERLAY_BREAKPOINT).toBe(640);
|
|
109
|
+
expect(OVERLAY_BREAKPOINT).toBe(layoutBreakpoints.medium);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("no viewport can mix a desktop grid with a sheet overlay", () => {
|
|
113
|
+
// The 700px case from the drift register: DataTable reads `media.sm`
|
|
114
|
+
// (v5 minWidth 640) and FloatingPanel reads OVERLAY_BREAKPOINT. Equal
|
|
115
|
+
// thresholds means one idiom per width, by construction.
|
|
116
|
+
const dataTableGridFrom = 640;
|
|
117
|
+
expect(OVERLAY_BREAKPOINT).toBe(dataTableGridFrom);
|
|
118
|
+
for (const width of [500, 639, 700, 800, 900]) {
|
|
119
|
+
const tableIsGrid = width >= dataTableGridFrom;
|
|
120
|
+
const overlayFloats = width > OVERLAY_BREAKPOINT;
|
|
121
|
+
// The only disagreement allowed is the single boundary pixel row
|
|
122
|
+
// (>= vs >), never a 220px band.
|
|
123
|
+
expect(tableIsGrid === overlayFloats || width === 640).toBe(true);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("expanded 860 survives as the pane-budget class only", () => {
|
|
128
|
+
expect(layoutBreakpoints.expanded).toBe(860);
|
|
129
|
+
expect(OVERLAY_BREAKPOINT).not.toBe(layoutBreakpoints.expanded);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("MPO-13 — the touch floor stops fighting the desktop ramp", () => {
|
|
134
|
+
it("$4 meets the 44 floor natively; touch lifts it, never shrinks it", () => {
|
|
135
|
+
const desktop = sizeRecipeForToken("$4");
|
|
136
|
+
const touch = sizeRecipeForToken("$4", { touch: true });
|
|
137
|
+
expect(desktop.height).toBe(DEFAULT_SIZE_RECIPE_INPUTS.touchFloor);
|
|
138
|
+
expect(touch.height).toBeGreaterThanOrEqual(desktop.height);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("every touch height clears the floor", () => {
|
|
142
|
+
const { heightsTouch } = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
|
|
143
|
+
for (const [token, px] of Object.entries(heightsTouch)) {
|
|
144
|
+
expect(px, `heightsTouch.${token}`).toBeGreaterThanOrEqual(
|
|
145
|
+
DEFAULT_SIZE_RECIPE_INPUTS.touchFloor,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -9,11 +9,21 @@
|
|
|
9
9
|
* Tamagui `createThemes` / theme-builder still emits COLOR palettes only.
|
|
10
10
|
* Do not stuff recipe keys into `getTheme()` / `themes`.
|
|
11
11
|
*
|
|
12
|
-
* DEFAULT uses `tokenHeights`
|
|
13
|
-
* ($4=
|
|
14
|
-
*
|
|
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`.
|
|
15
17
|
*/
|
|
16
18
|
|
|
19
|
+
import {
|
|
20
|
+
cloneTileSizeInputs,
|
|
21
|
+
generateTileSizeLadder,
|
|
22
|
+
resolveTileSizeInputs,
|
|
23
|
+
type TileSizeInputs,
|
|
24
|
+
type TileSizeLadder,
|
|
25
|
+
} from "./tileSizes";
|
|
26
|
+
|
|
17
27
|
export const SIZE_RECIPE_TOKENS = [
|
|
18
28
|
"$1",
|
|
19
29
|
"$1.5",
|
|
@@ -55,6 +65,11 @@ export type SizeRecipeInputs = {
|
|
|
55
65
|
compactRatios: SizeRecipeRatios;
|
|
56
66
|
radiusPx: number;
|
|
57
67
|
touchFloor: number;
|
|
68
|
+
/**
|
|
69
|
+
* Tile-size ladder inputs (MPO-31). Merged over `defaultTileSizeInputs`;
|
|
70
|
+
* the ladder table derives from these the way every other table does.
|
|
71
|
+
*/
|
|
72
|
+
tile?: Partial<TileSizeInputs>;
|
|
58
73
|
};
|
|
59
74
|
|
|
60
75
|
/** @deprecated Use `SizeRecipeInputs`. */
|
|
@@ -104,6 +119,8 @@ export type GeneratedSizeRecipes = {
|
|
|
104
119
|
typeVariants: Record<string, SizeRecipeTypeVariant>;
|
|
105
120
|
iconVariants: Record<string, SizeRecipeIconVariant>;
|
|
106
121
|
families: RecipeFamilies;
|
|
122
|
+
/** Tile-size ladder (MPO-31) — derived from `inputs.tile` over the measured defaults. */
|
|
123
|
+
tiles: TileSizeLadder;
|
|
107
124
|
};
|
|
108
125
|
|
|
109
126
|
export const SIZE_TOKEN_EXPONENTS: Record<SizeRecipeToken, number> = {
|
|
@@ -119,56 +136,73 @@ export const SIZE_TOKEN_EXPONENTS: Record<SizeRecipeToken, number> = {
|
|
|
119
136
|
$6: 2,
|
|
120
137
|
};
|
|
121
138
|
|
|
139
|
+
// Stock Tamagui `$size` px as measured from the live tamagui.dev CSS vars
|
|
140
|
+
// (MPO-13 fidelity pass): $1:20 $1.5:24 $2:28 $2.5:32 $3:36 $3.5:40
|
|
141
|
+
// $4/$true:44 $5:52 $6:64. The recipe key "$4" and the raw Tamagui token
|
|
142
|
+
// `$size.4` resolve to the SAME height — the C1 split (recipe 32 vs token
|
|
143
|
+
// 44) is gone.
|
|
122
144
|
const DEFAULT_TOKEN_HEIGHTS: Record<string, number> = {
|
|
123
145
|
$1: 20,
|
|
124
146
|
"$1.5": 24,
|
|
125
|
-
$2:
|
|
126
|
-
"$2.5":
|
|
127
|
-
$3:
|
|
128
|
-
"$3.5":
|
|
129
|
-
$4:
|
|
130
|
-
$true:
|
|
131
|
-
$5:
|
|
132
|
-
$6:
|
|
147
|
+
$2: 28,
|
|
148
|
+
"$2.5": 32,
|
|
149
|
+
$3: 36,
|
|
150
|
+
"$3.5": 40,
|
|
151
|
+
$4: 44,
|
|
152
|
+
$true: 44,
|
|
153
|
+
$5: 52,
|
|
154
|
+
$6: 64,
|
|
133
155
|
};
|
|
134
156
|
|
|
135
|
-
/** Lossless relative table:
|
|
157
|
+
/** Lossless relative table: the stock Tamagui 20/24/28/32/36/40/44/52/64 ramp at baseHeight 44. */
|
|
136
158
|
export const defaultSizeFactors: Record<SizeRecipeToken, number> = {
|
|
137
|
-
$1: 20 /
|
|
138
|
-
"$1.5": 24 /
|
|
139
|
-
$2:
|
|
140
|
-
"$2.5":
|
|
141
|
-
$3:
|
|
142
|
-
"$3.5":
|
|
159
|
+
$1: 20 / 44,
|
|
160
|
+
"$1.5": 24 / 44,
|
|
161
|
+
$2: 28 / 44,
|
|
162
|
+
"$2.5": 32 / 44,
|
|
163
|
+
$3: 36 / 44,
|
|
164
|
+
"$3.5": 40 / 44,
|
|
143
165
|
$4: 1,
|
|
144
166
|
$true: 1,
|
|
145
|
-
$5:
|
|
146
|
-
$6:
|
|
167
|
+
$5: 52 / 44,
|
|
168
|
+
$6: 64 / 44,
|
|
147
169
|
};
|
|
148
170
|
|
|
171
|
+
// Anchored at the measured $4/$true control: height 44, padX 18
|
|
172
|
+
// ($space.true), font 14, icon 16, gap 9 (gap tracks $radius.true). The
|
|
173
|
+
// ratio model is linear, so steps away from $4 approximate Tamagui's
|
|
174
|
+
// hand-tuned ramp rather than matching it exactly.
|
|
149
175
|
const DEFAULT_RATIOS: SizeRecipeRatios = {
|
|
150
|
-
padX:
|
|
151
|
-
font: 14 /
|
|
152
|
-
icon: 16 /
|
|
153
|
-
gap:
|
|
176
|
+
padX: 18 / 44,
|
|
177
|
+
font: 14 / 44,
|
|
178
|
+
icon: 16 / 44,
|
|
179
|
+
gap: 9 / 44,
|
|
154
180
|
};
|
|
155
181
|
|
|
182
|
+
// One space step and one type step below control at the same height:
|
|
183
|
+
// $4 compact = padX 13 ($space.3), font 13 ($3 label), icon 14, gap 7
|
|
184
|
+
// ($space.2).
|
|
156
185
|
const DEFAULT_COMPACT_RATIOS: SizeRecipeRatios = {
|
|
157
|
-
padX:
|
|
158
|
-
font: 13 /
|
|
159
|
-
icon: 14 /
|
|
160
|
-
gap:
|
|
186
|
+
padX: 13 / 44,
|
|
187
|
+
font: 13 / 44,
|
|
188
|
+
icon: 14 / 44,
|
|
189
|
+
gap: 7 / 44,
|
|
161
190
|
};
|
|
162
191
|
|
|
163
192
|
export const DEFAULT_SIZE_RECIPE_INPUTS: SizeRecipeInputs = {
|
|
164
|
-
|
|
193
|
+
// Tamagui `$size.true` = 44 — the iOS touch minimum, measured on every
|
|
194
|
+
// tamagui.dev button and input (MPO-13 C2; was 32).
|
|
195
|
+
baseHeight: 44,
|
|
165
196
|
stepRatio: 1.25,
|
|
166
197
|
tokenSteps: { ...SIZE_TOKEN_EXPONENTS },
|
|
167
198
|
tokenHeights: { ...DEFAULT_TOKEN_HEIGHTS },
|
|
168
199
|
sizeFactors: { ...defaultSizeFactors },
|
|
169
200
|
ratios: { ...DEFAULT_RATIOS },
|
|
170
201
|
compactRatios: { ...DEFAULT_COMPACT_RATIOS },
|
|
171
|
-
|
|
202
|
+
// Tamagui `$radius.true` = 9, matching the borderRadius knob's medium
|
|
203
|
+
// stop ($4 = 9px) — the stored input no longer contradicts the knob
|
|
204
|
+
// (MPO-13 C3; was 6).
|
|
205
|
+
radiusPx: 9,
|
|
172
206
|
touchFloor: 44,
|
|
173
207
|
};
|
|
174
208
|
|
|
@@ -186,6 +220,7 @@ export function cloneSizeRecipeInputs(inputs: SizeRecipeInputs): SizeRecipeInput
|
|
|
186
220
|
sizeFactors: inputs.sizeFactors ? { ...inputs.sizeFactors } : undefined,
|
|
187
221
|
ratios: { ...inputs.ratios },
|
|
188
222
|
compactRatios: { ...inputs.compactRatios },
|
|
223
|
+
tile: inputs.tile ? cloneTileSizeInputs(inputs.tile) : undefined,
|
|
189
224
|
};
|
|
190
225
|
}
|
|
191
226
|
|
|
@@ -209,6 +244,10 @@ export function resolveSizeRecipeInputs(partial: Partial<SizeRecipeInputs> = {})
|
|
|
209
244
|
touchFloor: partial.touchFloor ?? DEFAULT_SIZE_RECIPE_INPUTS.touchFloor,
|
|
210
245
|
};
|
|
211
246
|
|
|
247
|
+
if (partial.tile !== undefined) {
|
|
248
|
+
resolved.tile = partial.tile ? resolveTileSizeInputs(partial.tile) : undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
212
251
|
if (hasTokenHeights) {
|
|
213
252
|
resolved.tokenHeights = partial.tokenHeights ? { ...partial.tokenHeights } : undefined;
|
|
214
253
|
if (hasSizeFactors) {
|
|
@@ -364,5 +403,6 @@ export function generateSizeRecipes(partial: Partial<SizeRecipeInputs> = {}): Ge
|
|
|
364
403
|
typeVariants: control.typeVariants,
|
|
365
404
|
iconVariants: control.iconVariants,
|
|
366
405
|
families,
|
|
406
|
+
tiles: generateTileSizeLadder(resolveTileSizeInputs(inputs.tile ?? {})),
|
|
367
407
|
};
|
|
368
408
|
}
|
|
@@ -398,9 +398,9 @@ describe("resolveKnobs", () => {
|
|
|
398
398
|
it("fills control / controlType / controlIcon from the size recipe", () => {
|
|
399
399
|
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
400
400
|
expect(knobProps.control).toEqual({
|
|
401
|
-
height:
|
|
402
|
-
paddingHorizontal:
|
|
403
|
-
gap:
|
|
401
|
+
height: 44,
|
|
402
|
+
paddingHorizontal: 18,
|
|
403
|
+
gap: 9,
|
|
404
404
|
});
|
|
405
405
|
expect(knobProps.controlType).toEqual({ fontSize: 14 });
|
|
406
406
|
expect(knobProps.controlIcon).toEqual({ width: 16, height: 16 });
|
|
@@ -412,8 +412,8 @@ describe("resolveKnobs", () => {
|
|
|
412
412
|
it("small and large share the same borderRadius fragment", () => {
|
|
413
413
|
const small = resolveKnobs(knobsWith({ size: "small" }));
|
|
414
414
|
const large = resolveKnobs(knobsWith({ size: "large" }));
|
|
415
|
-
expect(small.knobProps.control.height).toBe(
|
|
416
|
-
expect(large.knobProps.control.height).toBe(
|
|
415
|
+
expect(small.knobProps.control.height).toBe(36);
|
|
416
|
+
expect(large.knobProps.control.height).toBe(52);
|
|
417
417
|
expect(small.knobProps.borderRadius).toEqual(large.knobProps.borderRadius);
|
|
418
418
|
});
|
|
419
419
|
});
|
|
@@ -37,23 +37,23 @@ function expectNoRadius(value: object) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
describe("size recipes", () => {
|
|
40
|
-
it("medium / $4 matches the
|
|
40
|
+
it("medium / $4 matches the measured tamagui.dev 44/18/14/16/9 reference", () => {
|
|
41
41
|
const recipe = sizeRecipes.$4;
|
|
42
42
|
expect(recipe).toEqual({
|
|
43
|
-
height:
|
|
44
|
-
paddingHorizontal:
|
|
43
|
+
height: 44,
|
|
44
|
+
paddingHorizontal: 18,
|
|
45
45
|
fontSize: 14,
|
|
46
46
|
iconSize: 16,
|
|
47
|
-
gap:
|
|
47
|
+
gap: 9,
|
|
48
48
|
});
|
|
49
|
-
expect(sizeRecipeFromHeight(
|
|
49
|
+
expect(sizeRecipeFromHeight(44)).toEqual(recipe);
|
|
50
50
|
expect(sizeRecipeForToken("$4")).toEqual(recipe);
|
|
51
|
-
expect(SIZE_RECIPE_HEIGHTS.$4).toBe(
|
|
51
|
+
expect(SIZE_RECIPE_HEIGHTS.$4).toBe(44);
|
|
52
52
|
expect(SIZE_RECIPE_RATIOS).toEqual({
|
|
53
|
-
padX:
|
|
54
|
-
font: 14 /
|
|
55
|
-
icon: 16 /
|
|
56
|
-
gap:
|
|
53
|
+
padX: 18 / 44,
|
|
54
|
+
font: 14 / 44,
|
|
55
|
+
icon: 16 / 44,
|
|
56
|
+
gap: 9 / 44,
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -65,7 +65,7 @@ describe("size recipes", () => {
|
|
|
65
65
|
|
|
66
66
|
it("radius is not on the recipe or any variant object", () => {
|
|
67
67
|
expectNoRadius(sizeRecipes.$4);
|
|
68
|
-
expectNoRadius(sizeRecipeFromHeight(
|
|
68
|
+
expectNoRadius(sizeRecipeFromHeight(44));
|
|
69
69
|
expectNoRadius(sizeRecipeForToken("$4"));
|
|
70
70
|
for (const token of RECIPE_KEYS) {
|
|
71
71
|
expectNoRadius(sizeRecipes[token]);
|
|
@@ -144,13 +144,13 @@ describe("size recipes", () => {
|
|
|
144
144
|
it("touch heights floor at 44 and still differentiate size steps", () => {
|
|
145
145
|
const desktop = sizeRecipeForToken("$4");
|
|
146
146
|
expect(desktop).toEqual(sizeRecipes.$4);
|
|
147
|
-
expect(applyTouchFloor(desktop, false).height).toBe(
|
|
147
|
+
expect(applyTouchFloor(desktop, false).height).toBe(44);
|
|
148
148
|
expect(applyTouchFloor(desktop, true)).toEqual(sizeRecipeFromHeight(48));
|
|
149
149
|
expect(sizeRecipeForToken("$4", { touch: true })).toEqual(sizeRecipeFromHeight(48));
|
|
150
150
|
expect(sizeRecipeForToken("$3", { touch: true }).height).toBe(44);
|
|
151
151
|
expect(sizeRecipeForToken("$5", { touch: true }).height).toBe(56);
|
|
152
|
-
expect(sizeRecipeForToken("$6", { touch: true }).height).toBe(
|
|
153
|
-
expect(sizeRecipeBoxVariants.$4.height).toBe(
|
|
152
|
+
expect(sizeRecipeForToken("$6", { touch: true }).height).toBe(68);
|
|
153
|
+
expect(sizeRecipeBoxVariants.$4.height).toBe(44);
|
|
154
154
|
expectNoRadius(applyTouchFloor(desktop, true));
|
|
155
155
|
});
|
|
156
156
|
});
|
|
@@ -168,29 +168,29 @@ describe("generateSizeRecipes (LC-70)", () => {
|
|
|
168
168
|
setSizeRecipeInputs();
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
it("DEFAULT $4 is
|
|
171
|
+
it("DEFAULT $4 is the measured 44/18/14/16/9 with no radius", () => {
|
|
172
172
|
const generated = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
|
|
173
173
|
expect(generated.recipes.$4).toEqual({
|
|
174
|
-
height:
|
|
175
|
-
paddingHorizontal:
|
|
174
|
+
height: 44,
|
|
175
|
+
paddingHorizontal: 18,
|
|
176
176
|
fontSize: 14,
|
|
177
177
|
iconSize: 16,
|
|
178
|
-
gap:
|
|
178
|
+
gap: 9,
|
|
179
179
|
});
|
|
180
180
|
expectNoRadius(generated.recipes.$4);
|
|
181
181
|
expectNoRadius(generated.boxVariants.$4);
|
|
182
|
-
expect(generated.inputs.radiusPx).toBe(
|
|
182
|
+
expect(generated.inputs.radiusPx).toBe(9);
|
|
183
183
|
expect(generated.heights).toEqual({
|
|
184
184
|
$1: 20,
|
|
185
185
|
"$1.5": 24,
|
|
186
|
-
$2:
|
|
187
|
-
"$2.5":
|
|
188
|
-
$3:
|
|
189
|
-
"$3.5":
|
|
190
|
-
$4:
|
|
191
|
-
$true:
|
|
192
|
-
$5:
|
|
193
|
-
$6:
|
|
186
|
+
$2: 28,
|
|
187
|
+
"$2.5": 32,
|
|
188
|
+
$3: 36,
|
|
189
|
+
"$3.5": 40,
|
|
190
|
+
$4: 44,
|
|
191
|
+
$true: 44,
|
|
192
|
+
$5: 52,
|
|
193
|
+
$6: 64,
|
|
194
194
|
});
|
|
195
195
|
expect(generated.heightsTouch).toEqual({
|
|
196
196
|
$1: 44,
|
|
@@ -202,9 +202,11 @@ describe("generateSizeRecipes (LC-70)", () => {
|
|
|
202
202
|
$4: 48,
|
|
203
203
|
$true: 48,
|
|
204
204
|
$5: 56,
|
|
205
|
-
$6:
|
|
205
|
+
$6: 68,
|
|
206
206
|
});
|
|
207
|
-
expect(generateSizeRecipeTables(defaultRecipeInputs).touchHeights).toEqual(
|
|
207
|
+
expect(generateSizeRecipeTables(defaultRecipeInputs).touchHeights).toEqual(
|
|
208
|
+
generated.heightsTouch,
|
|
209
|
+
);
|
|
208
210
|
expect(SIZE_RECIPE_HEIGHTS_TOUCH.$4).toBe(48);
|
|
209
211
|
});
|
|
210
212
|
|
|
@@ -214,10 +216,10 @@ describe("generateSizeRecipes (LC-70)", () => {
|
|
|
214
216
|
expect(generated.heights.$4).toBe(40);
|
|
215
217
|
expect(generated.recipes.$4).toEqual({
|
|
216
218
|
height: 40,
|
|
217
|
-
paddingHorizontal: Math.round(40 * (
|
|
218
|
-
fontSize: Math.round(40 * (14 /
|
|
219
|
-
iconSize: Math.round(40 * (16 /
|
|
220
|
-
gap: Math.round(40 * (
|
|
219
|
+
paddingHorizontal: Math.round(40 * (18 / 44)),
|
|
220
|
+
fontSize: Math.round(40 * (14 / 44)),
|
|
221
|
+
iconSize: Math.round(40 * (16 / 44)),
|
|
222
|
+
gap: Math.round(40 * (9 / 44)),
|
|
221
223
|
});
|
|
222
224
|
expect(generated.heights.$5).not.toBe(generated.heights.$4);
|
|
223
225
|
expectNoRadius(generated.recipes.$4);
|
|
@@ -225,10 +227,10 @@ describe("generateSizeRecipes (LC-70)", () => {
|
|
|
225
227
|
|
|
226
228
|
it("controlCompact padX is tighter than control at the same $4 height", () => {
|
|
227
229
|
const generated = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
|
|
228
|
-
expect(generated.families.control.recipes.$4.height).toBe(
|
|
229
|
-
expect(generated.families.controlCompact.recipes.$4.height).toBe(
|
|
230
|
-
expect(generated.families.control.recipes.$4.paddingHorizontal).toBe(
|
|
231
|
-
expect(generated.families.controlCompact.recipes.$4.paddingHorizontal).toBe(
|
|
230
|
+
expect(generated.families.control.recipes.$4.height).toBe(44);
|
|
231
|
+
expect(generated.families.controlCompact.recipes.$4.height).toBe(44);
|
|
232
|
+
expect(generated.families.control.recipes.$4.paddingHorizontal).toBe(18);
|
|
233
|
+
expect(generated.families.controlCompact.recipes.$4.paddingHorizontal).toBe(13);
|
|
232
234
|
expect(generated.families.controlCompact.recipes.$4.paddingHorizontal).toBeLessThan(
|
|
233
235
|
generated.families.control.recipes.$4.paddingHorizontal,
|
|
234
236
|
);
|
|
@@ -275,9 +277,9 @@ describe("size recipe families", () => {
|
|
|
275
277
|
expect(control.heightVariants[token]).toEqual({ height: SIZE_RECIPE_HEIGHTS[token] });
|
|
276
278
|
}
|
|
277
279
|
expect(control.boxVariants.$4).toEqual({
|
|
278
|
-
height:
|
|
279
|
-
paddingHorizontal:
|
|
280
|
-
gap:
|
|
280
|
+
height: 44,
|
|
281
|
+
paddingHorizontal: 18,
|
|
282
|
+
gap: 9,
|
|
281
283
|
});
|
|
282
284
|
});
|
|
283
285
|
|
|
@@ -289,8 +291,8 @@ describe("size recipe families", () => {
|
|
|
289
291
|
expect(compact.boxVariants.$4.paddingHorizontal).toBeLessThan(
|
|
290
292
|
control.boxVariants.$4.paddingHorizontal,
|
|
291
293
|
);
|
|
292
|
-
expect(compact.boxVariants.$4.paddingHorizontal).toBe(
|
|
293
|
-
expect(compact.boxVariants.$4.gap).toBe(
|
|
294
|
+
expect(compact.boxVariants.$4.paddingHorizontal).toBe(13);
|
|
295
|
+
expect(compact.boxVariants.$4.gap).toBe(7);
|
|
294
296
|
expect(sizeRecipeForToken("$4", { family: "controlCompact" })).toEqual(compact.recipes.$4);
|
|
295
297
|
});
|
|
296
298
|
});
|