@multiplatform.one/theme 7.20.0 → 7.22.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.22.0",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@multiplatform.one/platform": "7.
|
|
48
|
-
"@multiplatform.one/store": "7.
|
|
47
|
+
"@multiplatform.one/platform": "7.22.0",
|
|
48
|
+
"@multiplatform.one/store": "7.22.0",
|
|
49
49
|
"@tamagui/colors": "2.7.6",
|
|
50
50
|
"@tamagui/config": "2.7.6",
|
|
51
51
|
"@tamagui/get-token": "2.7.6",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"react-cookie": "^8.1.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@multiplatform.one/config": "7.
|
|
61
|
-
"@multiplatform.one/test-utils": "7.
|
|
60
|
+
"@multiplatform.one/config": "7.22.0",
|
|
61
|
+
"@multiplatform.one/test-utils": "7.22.0",
|
|
62
62
|
"@tamagui/animations-css": "2.7.6",
|
|
63
63
|
"@tamagui/animations-reanimated": "2.7.6",
|
|
64
64
|
"@tamagui/font-inter": "2.7.6",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-95, ruled C by Clay 2026-09-26: mpo's neutral ramp is its own hue-270
|
|
3
|
+
* ramp, signed off as a deliberate divergence from tamagui.dev's achromatic
|
|
4
|
+
* grays. This pins the design (the stops in `base.ts`), what the builder makes
|
|
5
|
+
* of it (it quantizes each stop to integer hsla, so 56.5% renders as 56%), and
|
|
6
|
+
* the file the gallery boards and their verifiers read. Changing the ramp is a
|
|
7
|
+
* new ruling: update the rulebook, the JSON and the gallery token block with it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { createThemesBuilder } from "../createThemes";
|
|
14
|
+
import { defaultAccentTheme } from "./accent";
|
|
15
|
+
import { defaultBaseTheme } from "./base";
|
|
16
|
+
import { defaultBuilderOptions } from "./builderOptions";
|
|
17
|
+
|
|
18
|
+
const signedOffLightness = {
|
|
19
|
+
light: [99.5, 98.2, 95.2, 92.5, 90, 87, 82.5, 75, 56.5, 46, 40, 14],
|
|
20
|
+
dark: [8.7, 10.5, 13.5, 16, 18.5, 21.5, 25.5, 32, 43, 54, 62.5, 94],
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read off the rendered FileUpload dropzone (Storybook, Chromium, DPR 2) when
|
|
25
|
+
* MPO-95 was filed. These are what a user sees; everything else is derived.
|
|
26
|
+
*/
|
|
27
|
+
const measured = [
|
|
28
|
+
["light", 3, "rgb(243, 242, 243)"],
|
|
29
|
+
["light", 6, "rgb(222, 220, 224)"],
|
|
30
|
+
["light", 9, "rgb(143, 136, 150)"],
|
|
31
|
+
["dark", 9, "rgb(110, 103, 116)"],
|
|
32
|
+
["light", 12, "rgb(36, 34, 38)"],
|
|
33
|
+
["dark", 12, "rgb(240, 239, 241)"],
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
type Scheme = "light" | "dark";
|
|
37
|
+
type Themes = Record<string, Record<string, string>>;
|
|
38
|
+
|
|
39
|
+
const built = createThemesBuilder(
|
|
40
|
+
defaultBaseTheme,
|
|
41
|
+
defaultAccentTheme,
|
|
42
|
+
defaultBuilderOptions,
|
|
43
|
+
).themes() as Themes;
|
|
44
|
+
|
|
45
|
+
// Vite rewrites import.meta.url to an http URL; resolve from cwd (public/theme).
|
|
46
|
+
const rampFile = JSON.parse(
|
|
47
|
+
readFileSync(join(process.cwd(), "../../docs/design/measured/mpo-neutral-ramp.json"), "utf8"),
|
|
48
|
+
) as Record<Scheme, string[]>;
|
|
49
|
+
|
|
50
|
+
const OPAQUE_HSL = /^hsla?\(\s*([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%(?:,\s*1)?\s*\)$/;
|
|
51
|
+
|
|
52
|
+
function hslaToRgb(value: string): [number, number, number] {
|
|
53
|
+
const match = value.match(OPAQUE_HSL);
|
|
54
|
+
if (!match) throw new Error(`not an opaque hsl(a) colour: ${value}`);
|
|
55
|
+
const [h, s, l] = [Number(match[1]), Number(match[2]) / 100, Number(match[3]) / 100];
|
|
56
|
+
const chroma = (1 - Math.abs(2 * l - 1)) * s;
|
|
57
|
+
const x = chroma * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
58
|
+
const m = l - chroma / 2;
|
|
59
|
+
const sector = Math.floor(h / 60) % 6;
|
|
60
|
+
const [r, g, b] = [
|
|
61
|
+
[chroma, x, 0],
|
|
62
|
+
[x, chroma, 0],
|
|
63
|
+
[0, chroma, x],
|
|
64
|
+
[0, x, chroma],
|
|
65
|
+
[x, 0, chroma],
|
|
66
|
+
[chroma, 0, x],
|
|
67
|
+
][sector];
|
|
68
|
+
return [r, g, b].map((channel) => Math.round((channel + m) * 255)) as [number, number, number];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const toHex = (value: string) =>
|
|
72
|
+
`#${hslaToRgb(value)
|
|
73
|
+
.map((channel) => channel.toString(16).padStart(2, "0"))
|
|
74
|
+
.join("")}`;
|
|
75
|
+
|
|
76
|
+
const builtRamp = (scheme: Scheme) =>
|
|
77
|
+
Array.from({ length: 12 }, (_, i) => built[scheme][`color${i + 1}`]);
|
|
78
|
+
|
|
79
|
+
describe("MPO-95: the neutral ramp is mpo's hue-270 ramp", () => {
|
|
80
|
+
it("base.ts declares the signed-off stops: hue 270, 6% saturation, twelve lightnesses", () => {
|
|
81
|
+
expect(defaultBaseTheme.lightPalette).toEqual(
|
|
82
|
+
signedOffLightness.light.map((l) => `hsla(270, 6%, ${l}%, 1)`),
|
|
83
|
+
);
|
|
84
|
+
expect(defaultBaseTheme.darkPalette).toEqual(
|
|
85
|
+
signedOffLightness.dark.map((l) => `hsla(270, 6%, ${l}%, 1)`),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const [scheme, step, rgb] of measured) {
|
|
90
|
+
it(`${scheme} $color${step} builds to the measured ${rgb}`, () => {
|
|
91
|
+
expect(`rgb(${hslaToRgb(built[scheme][`color${step}`]).join(", ")})`).toBe(rgb);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
96
|
+
it(`the built ${scheme} ramp is mpo-neutral-ramp.json, step for step`, () => {
|
|
97
|
+
expect(builtRamp(scheme).map(toHex)).toEqual(rampFile[scheme]);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
it("the tint survives the builder's rounding: only light $color1 lands on pure white", () => {
|
|
102
|
+
const achromatic = (hex: string) =>
|
|
103
|
+
new Set([hex.slice(1, 3), hex.slice(3, 5), hex.slice(5, 7)]).size === 1;
|
|
104
|
+
expect(rampFile.light.filter(achromatic)).toEqual(["#ffffff"]);
|
|
105
|
+
expect(rampFile.dark.filter(achromatic)).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -577,6 +577,74 @@ describe("resolveKnobs", () => {
|
|
|
577
577
|
expect(knobProps.featureSurface.shadowRadius).toBe(12);
|
|
578
578
|
expect(knobProps.featureSurface).not.toHaveProperty("borderWidth");
|
|
579
579
|
});
|
|
580
|
+
|
|
581
|
+
it("featureSurface keeps $10 / $6 at the default knobs and every other stop moves them (MPO-21 GL-59)", () => {
|
|
582
|
+
expect(defaultKnobs.borderRadius).toBe("medium");
|
|
583
|
+
expect(defaultKnobs.space).toBe("medium");
|
|
584
|
+
for (const borderRadius of ["none", "small", "large", "full"] as const) {
|
|
585
|
+
expect(
|
|
586
|
+
resolveKnobs(knobsWith({ borderRadius })).knobProps.featureSurface.borderRadius,
|
|
587
|
+
borderRadius,
|
|
588
|
+
).not.toBe("$10");
|
|
589
|
+
}
|
|
590
|
+
for (const space of ["small", "large"] as const) {
|
|
591
|
+
expect(resolveKnobs(knobsWith({ space })).knobProps.featureSurface.padding, space).not.toBe(
|
|
592
|
+
"$6",
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("featureSurface radius follows the radius knob, one token per stop around $10, uncapped by space", () => {
|
|
598
|
+
const expected = {
|
|
599
|
+
none: "$0",
|
|
600
|
+
small: "$9",
|
|
601
|
+
medium: "$10",
|
|
602
|
+
large: "$11",
|
|
603
|
+
full: "$12",
|
|
604
|
+
} as const;
|
|
605
|
+
for (const [borderRadius, token] of Object.entries(expected)) {
|
|
606
|
+
for (const space of ["small", "medium", "large"] as const) {
|
|
607
|
+
expect(
|
|
608
|
+
resolveKnobs(knobsWith({ borderRadius: borderRadius as Knobs["borderRadius"], space }))
|
|
609
|
+
.knobProps.featureSurface.borderRadius,
|
|
610
|
+
`${borderRadius}/${space}`,
|
|
611
|
+
).toBe(token);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("featureSurface padding follows the space knob, one token per stop around $6", () => {
|
|
617
|
+
const expected = { small: "$5", medium: "$6", large: "$7" } as const;
|
|
618
|
+
for (const [space, token] of Object.entries(expected)) {
|
|
619
|
+
for (const borderRadius of ["none", "small", "medium", "large", "full"] as const) {
|
|
620
|
+
expect(
|
|
621
|
+
resolveKnobs(knobsWith({ borderRadius, space: space as Knobs["space"] })).knobProps
|
|
622
|
+
.featureSurface.padding,
|
|
623
|
+
`${borderRadius}/${space}`,
|
|
624
|
+
).toBe(token);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("compact density steps featureSurface padding through the space knob", () => {
|
|
630
|
+
const { knobProps } = resolveKnobs(applyDensity(knobsWith({ density: "compact" })));
|
|
631
|
+
expect(knobProps.featureSurface.padding).toBe("$5");
|
|
632
|
+
expect(knobProps.featureSurface.borderRadius).toBe("$10");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("featureSurface never clips a child at its inset: padding >= radius x (1 - 1/sqrt 2) at every stop pair", () => {
|
|
636
|
+
for (const borderRadius of ["none", "small", "medium", "large", "full"] as const) {
|
|
637
|
+
for (const space of ["small", "medium", "large"] as const) {
|
|
638
|
+
const { featureSurface } = resolveKnobs(knobsWith({ borderRadius, space })).knobProps;
|
|
639
|
+
const radiusPx = (tokens.radius as any)[featureSurface.borderRadius.slice(1)]
|
|
640
|
+
.val as number;
|
|
641
|
+
const paddingPx = (tokens.space as any)[featureSurface.padding.slice(1)].val as number;
|
|
642
|
+
expect(paddingPx, `${borderRadius}/${space}`).toBeGreaterThanOrEqual(
|
|
643
|
+
radiusPx * (1 - Math.SQRT1_2),
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
});
|
|
580
648
|
});
|
|
581
649
|
|
|
582
650
|
// ── container radius cap (Axiom 1 CLIP) ──────────────────
|
|
@@ -695,7 +763,7 @@ describe("resolveKnobs", () => {
|
|
|
695
763
|
}
|
|
696
764
|
});
|
|
697
765
|
|
|
698
|
-
it("smooth × none: nothing to smooth
|
|
766
|
+
it("smooth × none: nothing to smooth, featureSurface included", () => {
|
|
699
767
|
const { knobProps } = resolveKnobs(
|
|
700
768
|
knobsWith({ cornerSmoothing: "smooth", borderRadius: "none" }),
|
|
701
769
|
);
|
|
@@ -705,9 +773,7 @@ describe("resolveKnobs", () => {
|
|
|
705
773
|
expect(knobProps.containerRadius).not.toHaveProperty("className");
|
|
706
774
|
expect(knobProps.cardSurface).not.toHaveProperty("className");
|
|
707
775
|
expect(knobProps.elevatedSurface).not.toHaveProperty("className");
|
|
708
|
-
|
|
709
|
-
// exists at every knob value, so it smooths.
|
|
710
|
-
expect(knobProps.featureSurface.className).toBe(cornerSmoothClassName);
|
|
776
|
+
expect(knobProps.featureSurface).not.toHaveProperty("className");
|
|
711
777
|
});
|
|
712
778
|
|
|
713
779
|
it("smooth × full: pill/circle identity fragments ignore smoothing (R-BINARY/R-PILL)", () => {
|
|
@@ -114,6 +114,24 @@ const panelPaddingMap = {
|
|
|
114
114
|
large: "$6",
|
|
115
115
|
} as const;
|
|
116
116
|
|
|
117
|
+
// Feature tier (SF-FEATURE, MPO-21 GL-59 ruling B): the Bento recipe's $10
|
|
118
|
+
// radius and $6 inset at the default stops, one token per stop either side.
|
|
119
|
+
// Uncapped by padding: the tightest pair (full 50 over small 24) still keeps
|
|
120
|
+
// a child corner at the inset inside the arc (padding >= radius x 0.29).
|
|
121
|
+
const featureRadiusMap = {
|
|
122
|
+
none: "$0",
|
|
123
|
+
small: "$9",
|
|
124
|
+
medium: "$10",
|
|
125
|
+
large: "$11",
|
|
126
|
+
full: "$12",
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
129
|
+
const featurePaddingMap = {
|
|
130
|
+
small: "$5",
|
|
131
|
+
medium: "$6",
|
|
132
|
+
large: "$7",
|
|
133
|
+
} as const;
|
|
134
|
+
|
|
117
135
|
// ── Page-title step (D-11 hero-H1 dial) ───────────────────────
|
|
118
136
|
// The one canonical definition of how big a page title is. On the heading
|
|
119
137
|
// scale (1.4x Inter, defaults/fonts.ts) $8 = 32px and $10 = 64px, so
|
|
@@ -590,8 +608,7 @@ export function resolveKnobs(
|
|
|
590
608
|
// Capped container fragments stay plain arcs at every knob (the Axiom 1
|
|
591
609
|
// cap = padding px, which superellipse corners under-fill, so the cap
|
|
592
610
|
// remains a safe over-approximation) and smooth whenever a nonzero arc
|
|
593
|
-
// exists. featureSurface
|
|
594
|
-
// smooths whenever the knob says smooth.
|
|
611
|
+
// exists. featureSurface smooths on the same rule: `none` has no arc.
|
|
595
612
|
const smoothing = cornerSmoothingStyle[knobs.cornerSmoothing ?? "round"];
|
|
596
613
|
const scaleSmoothing =
|
|
597
614
|
knobs.borderRadius === "none" || knobs.borderRadius === "full" ? undefined : smoothing;
|
|
@@ -704,15 +721,15 @@ export function resolveKnobs(
|
|
|
704
721
|
},
|
|
705
722
|
featureSurface: {
|
|
706
723
|
backgroundColor: "$background",
|
|
707
|
-
borderRadius:
|
|
708
|
-
padding:
|
|
724
|
+
borderRadius: featureRadiusMap[knobs.borderRadius],
|
|
725
|
+
padding: featurePaddingMap[knobs.space],
|
|
709
726
|
// Bento marketing/auth card shadow: ~0px 9px 12px rgba(0,0,0,0.02) —
|
|
710
727
|
// softer/wider than any elevationMap token, so explicit shadow props.
|
|
711
728
|
shadowColor: "rgba(0,0,0,0.02)",
|
|
712
729
|
shadowOffset: { width: 0, height: 9 },
|
|
713
730
|
shadowOpacity: 1,
|
|
714
731
|
shadowRadius: 12,
|
|
715
|
-
...
|
|
732
|
+
...containerSmoothing,
|
|
716
733
|
},
|
|
717
734
|
inputSurface: { borderWidth: borderWidthValue },
|
|
718
735
|
panelPadding: { padding: paddingToken },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveKnobs.d.ts","sourceRoot":"","sources":["../../src/theme/resolveKnobs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,SAAS,CAAC;AAU9D,OAAO,KAAK,EAA8B,KAAK,EAAE,cAAc,EAAc,MAAM,SAAS,CAAC;AAE7F,OAAO,KAAK,EAOV,iBAAiB,EAEjB,aAAa,EAEd,MAAM,WAAW,CAAC;AAKnB;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAI/D,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAIlE,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,eAAe,CAAC,EAAE,OAAO,GAAG,KAAK,CAU3E;AAID,eAAO,MAAM,eAAe;;;;;;CAMlB,CAAC;
|
|
1
|
+
{"version":3,"file":"resolveKnobs.d.ts","sourceRoot":"","sources":["../../src/theme/resolveKnobs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,SAAS,CAAC;AAU9D,OAAO,KAAK,EAA8B,KAAK,EAAE,cAAc,EAAc,MAAM,SAAS,CAAC;AAE7F,OAAO,KAAK,EAOV,iBAAiB,EAEjB,aAAa,EAEd,MAAM,WAAW,CAAC;AAKnB;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAI/D,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAIlE,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,eAAe,CAAC,EAAE,OAAO,GAAG,KAAK,CAU3E;AAID,eAAO,MAAM,eAAe;;;;;;CAMlB,CAAC;AAuEX;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAErF;AA0BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,KAAK,CAAC,cAAc,CAAC,EACnC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,GACpB,MAAM,GAAG,MAAM,CAIjB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,SAAS,CAIrF;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC;AAErF,MAAM,WAAW,uBAAuB;IACtC,2BAA2B,EAAE,eAAe,CAAC;IAC7C,kBAAkB,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;IAC1C,iBAAiB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,OAAO,EACpB,KAAK,EAAE,MAAM,GACZ,uBAAuB,GAAG,SAAS,CAQrC;AAuFD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEL,CAAC;AAEjE,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEnB,CAAC;AAE1D,sFAAsF;AACtF,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AA8O3C,wBAAgB,YAAY,CAC1B,KAAK,EAAE,KAAK,EACZ,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,MAAM,GAAE,WAAqB,EAC7B,WAAW,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,aAAa,CAwNf"}
|