@multiplatform.one/theme 6.4.3 → 6.7.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": "6.4.3",
3
+ "version": "6.7.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.0.0-rc.41",
54
54
  "@tamagui/web": "2.0.0-rc.41",
55
55
  "react-cookie": "^8.1.2",
56
- "@multiplatform.one/store": "6.4.3",
57
- "@multiplatform.one/platform": "6.4.3"
56
+ "@multiplatform.one/store": "6.7.0",
57
+ "@multiplatform.one/platform": "6.7.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@tamagui/animations-css": "2.0.0-rc.41",
@@ -69,8 +69,8 @@
69
69
  "tamagui": "2.0.0-rc.41",
70
70
  "typescript": "~5.9.3",
71
71
  "vitest": "^4.1.5",
72
- "@multiplatform.one/config": "6.4.3",
73
- "@multiplatform.one/test-utils": "6.4.3"
72
+ "@multiplatform.one/test-utils": "6.7.0",
73
+ "@multiplatform.one/config": "6.7.0"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@tamagui/animations-css": "^2.0.0-rc",
@@ -92,7 +92,7 @@
92
92
  },
93
93
  "scripts": {
94
94
  "test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html",
95
- "typecheck": "tsc --noEmit",
95
+ "typecheck": "tsgo --noEmit",
96
96
  "build": "rm -rf types 2>/dev/null && tsc -p tsconfig.build.json"
97
97
  }
98
98
  }
package/src/dates.spec.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { formatAbsoluteDate, formatAbsoluteDateTime, formatRelativeTimestamp } from "./dates";
2
+ import {
3
+ formatAbsoluteDate,
4
+ formatAbsoluteDateTime,
5
+ formatDayLong,
6
+ formatDayOfMonth,
7
+ formatMonthYear,
8
+ formatRelativeTimestamp,
9
+ formatTimeOfDay,
10
+ getMonthNames,
11
+ getWeekdayNames,
12
+ parseCalendarDate,
13
+ type DateInput,
14
+ } from "./dates";
3
15
 
4
16
  // Pin locale so assertions don't depend on the host machine.
5
17
  const enUS = { locale: "en-US" } as const;
@@ -73,3 +85,148 @@ describe("formatRelativeTimestamp (feed/social register)", () => {
73
85
  expect(formatRelativeTimestamp("garbage", base)).toBe("garbage");
74
86
  });
75
87
  });
88
+
89
+ describe("formatTimeOfDay (time-of-day register)", () => {
90
+ it("renders hour and minutes, never seconds", () => {
91
+ expect(formatTimeOfDay(new Date(2026, 7, 12, 14, 30, 45), enUS)).toBe("2:30 PM");
92
+ });
93
+
94
+ it("drops minutes for hour ticks", () => {
95
+ expect(formatTimeOfDay(new Date(2026, 7, 12, 14, 0), { ...enUS, minutes: false })).toBe("2 PM");
96
+ });
97
+
98
+ it("accepts a clock-time string — the wire shape Frappe Time fields send", () => {
99
+ expect(formatTimeOfDay("14:30", enUS)).toBe("2:30 PM");
100
+ expect(formatTimeOfDay("14:30:00", enUS)).toBe("2:30 PM");
101
+ });
102
+
103
+ it("passes unparseable strings through and renders empty input empty", () => {
104
+ expect(formatTimeOfDay("later", enUS)).toBe("later");
105
+ expect(formatTimeOfDay(null, enUS)).toBe("");
106
+ });
107
+ });
108
+
109
+ describe("calendar chrome registers", () => {
110
+ it("speaks a long day name for headers and accessible names", () => {
111
+ expect(formatDayLong(new Date(2026, 2, 10), enUS)).toBe("Tuesday, March 10");
112
+ expect(formatDayLong(new Date(2026, 2, 10), { ...enUS, weekday: false })).toBe("March 10");
113
+ expect(formatDayLong(new Date(2026, 2, 10), { ...enUS, year: true })).toBe(
114
+ "Tuesday, March 10, 2026",
115
+ );
116
+ });
117
+
118
+ it("names months and weekdays for calendar grids", () => {
119
+ expect(getMonthNames(enUS)[0]).toBe("January");
120
+ expect(getMonthNames({ ...enUS, style: "short" })[2]).toBe("Mar");
121
+ expect(getWeekdayNames(enUS)).toEqual(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]);
122
+ expect(getWeekdayNames({ ...enUS, firstDay: 1 })[0]).toBe("Mon");
123
+ });
124
+ });
125
+
126
+ describe("axis-tick registers", () => {
127
+ it("drops the year entirely so ticks keep a stable width", () => {
128
+ expect(formatAbsoluteDate(new Date(2025, 2, 10), { ...enUS, now, year: "never" })).toBe(
129
+ "Mar 10",
130
+ );
131
+ });
132
+
133
+ it("renders month-year and day-of-month ticks", () => {
134
+ expect(formatMonthYear(new Date(2026, 2, 10), enUS)).toBe("Mar 2026");
135
+ expect(formatMonthYear(new Date(2026, 2, 10), { ...enUS, month: "long" })).toBe("March 2026");
136
+ expect(formatDayOfMonth(new Date(2026, 2, 10), enUS)).toBe("10");
137
+ });
138
+ });
139
+
140
+ // Spans UTC, two negative offsets and the far positive extreme (UTC+14), so a
141
+ // parse that leans on the host zone fails somewhere in this list.
142
+ const ZONES = ["UTC", "America/Los_Angeles", "America/New_York", "Pacific/Kiritimati"] as const;
143
+
144
+ function inZone<T>(timeZone: string, read: () => T): T {
145
+ const previous = process.env.TZ;
146
+ process.env.TZ = timeZone;
147
+ try {
148
+ return read();
149
+ } finally {
150
+ if (previous === undefined) delete process.env.TZ;
151
+ else process.env.TZ = previous;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Local wall-clock reading of a parse, taken INSIDE the zone — a Date is an
157
+ * instant, so its day and hour only mean something against an ambient zone.
158
+ */
159
+ function readLocal(timeZone: string, value: DateInput) {
160
+ return inZone(timeZone, () => {
161
+ const date = parseCalendarDate(value);
162
+ if (!date) return null;
163
+ const pad = (n: number) => String(n).padStart(2, "0");
164
+ return {
165
+ day: `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
166
+ clock: `${pad(date.getHours())}:${pad(date.getMinutes())}`,
167
+ };
168
+ });
169
+ }
170
+
171
+ describe("parseCalendarDate (day-grid parse)", () => {
172
+ it("reads a date-only string as the same calendar day in every timezone", () => {
173
+ for (const timeZone of ZONES) {
174
+ expect({ timeZone, ...readLocal(timeZone, "2026-03-10") }).toEqual({
175
+ timeZone,
176
+ day: "2026-03-10",
177
+ // Local midnight, which is what day-grid math tests for: Gantt's
178
+ // normalizeSpanEdges only grants a date-only end its inclusive last
179
+ // day when the edge sits exactly on midnight.
180
+ clock: "00:00",
181
+ });
182
+ }
183
+ });
184
+
185
+ it("guards the ECMA-262 default this exists to avoid", () => {
186
+ // `new Date("YYYY-MM-DD")` is UTC midnight, so the naive parse names the
187
+ // previous day west of UTC. If this ever stops shifting, the test above
188
+ // has stopped proving anything.
189
+ const naive = inZone("America/Los_Angeles", () => new Date("2026-03-10").getDate());
190
+ expect(naive).toBe(9);
191
+ });
192
+
193
+ it("keeps the timezone-aware parse for values that carry a time", () => {
194
+ for (const timeZone of ZONES) {
195
+ // No offset in the string — a local wall-clock reading, unchanged.
196
+ expect(readLocal(timeZone, "2026-03-10 14:30:00")).toEqual({
197
+ day: "2026-03-10",
198
+ clock: "14:30",
199
+ });
200
+ }
201
+ // An explicit offset names one instant, which lands on different local
202
+ // days by design — genuine datetime semantics are untouched.
203
+ expect(readLocal("UTC", "2026-03-10T00:00:00Z")).toEqual({ day: "2026-03-10", clock: "00:00" });
204
+ expect(readLocal("America/Los_Angeles", "2026-03-10T00:00:00Z")).toEqual({
205
+ day: "2026-03-09",
206
+ clock: "17:00", // PDT — March 10 is past the 2026 DST switch
207
+ });
208
+ });
209
+
210
+ it("passes Date instances and epoch millis through untouched", () => {
211
+ const instance = new Date(2026, 2, 10, 9, 15);
212
+ expect(parseCalendarDate(instance)).toBe(instance);
213
+ expect(parseCalendarDate(Date.UTC(2026, 2, 10))?.toISOString()).toBe(
214
+ "2026-03-10T00:00:00.000Z",
215
+ );
216
+ });
217
+
218
+ it("returns null for empty, unparseable and out-of-range input", () => {
219
+ expect(parseCalendarDate(null)).toBeNull();
220
+ expect(parseCalendarDate(undefined)).toBeNull();
221
+ expect(parseCalendarDate("")).toBeNull();
222
+ expect(parseCalendarDate("not-a-date")).toBeNull();
223
+ expect(parseCalendarDate(new Date("nonsense"))).toBeNull();
224
+ // Rolled over rather than rejected would silently render April 14.
225
+ expect(parseCalendarDate("2026-13-45")).toBeNull();
226
+ expect(parseCalendarDate("2026-02-30")).toBeNull();
227
+ });
228
+
229
+ it("keeps a two-digit-looking year literal instead of mapping it to 19xx", () => {
230
+ expect(inZone("UTC", () => parseCalendarDate("0050-03-10")?.getFullYear())).toBe(50);
231
+ });
232
+ });
package/src/dates.ts CHANGED
@@ -65,6 +65,43 @@ function coerceDateInput(value: DateInput): CoercedDate | null {
65
65
  return Number.isNaN(date.getTime()) ? null : { date, dateOnly: false };
66
66
  }
67
67
 
68
+ /**
69
+ * Parse a date value onto the LOCAL calendar day it names — the canonical
70
+ * parse for anything that will be positioned on a day grid (calendar cells,
71
+ * Gantt bars, date axes) rather than merely formatted.
72
+ *
73
+ * A date-only string ("YYYY-MM-DD") is a calendar date carrying no timezone,
74
+ * which is how Frappe sends `Date` fields. ECMA-262 parses that form as UTC
75
+ * midnight, so every zone west of UTC reads it as the *previous* local day,
76
+ * and no zone but UTC lands it on the local midnight that day-grid math tests
77
+ * for. Those are built from local parts instead.
78
+ *
79
+ * Values that carry a time ("YYYY-MM-DD HH:MM:SS", ISO with an offset, epoch
80
+ * millis) are genuine instants and keep the timezone-aware parse untouched.
81
+ *
82
+ * Returns null for empty and unparseable input, and for out-of-range parts
83
+ * ("2026-13-45") rather than silently rolling them into the next month.
84
+ */
85
+ export function parseCalendarDate(value: DateInput): Date | null {
86
+ if (value == null || value === "") return null;
87
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
88
+ if (typeof value === "number") {
89
+ const fromEpoch = new Date(value);
90
+ return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch;
91
+ }
92
+ const trimmed = value.trim();
93
+ if (ISO_DATE_ONLY.test(trimmed)) {
94
+ const [year, month, day] = trimmed.split("-").map(Number);
95
+ const date = new Date(year, month - 1, day);
96
+ // `new Date(50, …)` means 1950 — pin the year the string actually named.
97
+ if (year < 100) date.setFullYear(year);
98
+ if (date.getMonth() !== month - 1 || date.getDate() !== day) return null;
99
+ return date;
100
+ }
101
+ const parsed = new Date(trimmed.replace(" ", "T"));
102
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
103
+ }
104
+
68
105
  /**
69
106
  * Compact absolute date — the house register for every absolute date.
70
107
  * "Jul 1" / "Jul 1, 2025" (year auto-elided when current), or with
package/src/devWarn.ts CHANGED
@@ -32,6 +32,42 @@ export type DevWarnDetail = {
32
32
  [key: string]: unknown;
33
33
  };
34
34
 
35
+ /**
36
+ * LC-57 GUARDRAIL-CLEAN-STORIES allowlist entry, declared as a Storybook story
37
+ * parameter (`parameters.guardrailSpecimen`).
38
+ *
39
+ * Catalog stories emit zero `[theme]` guardrail warnings at rest. A story whose
40
+ * whole purpose is to demonstrate the warned state (a disabled-dial specimen, a
41
+ * pager boundary case) is allowlisted by declaring itself:
42
+ *
43
+ * ```ts
44
+ * export const Disabled: Story = {
45
+ * parameters: {
46
+ * guardrailSpecimen: {
47
+ * warns: ["bare-disabled"],
48
+ * reason: "Demonstrates the disabled dial; Stepper has no disabledReason plumbing.",
49
+ * } satisfies GuardrailSpecimenParameter,
50
+ * },
51
+ * };
52
+ * ```
53
+ *
54
+ * Semantics, pinned:
55
+ * - `warns` is the CLOSED set of codes the story may emit at rest. An emitted
56
+ * code outside the set still FAILs the story.
57
+ * - A declared code that happens not to fire passes — component improvements
58
+ * must never break an allowlist entry.
59
+ * - The declaration NEVER suppresses the console warning — the warn still
60
+ * fires; only the warn-harvester tolerates it (explicit, never tacit).
61
+ * - Harvesters (`p-results/story-hygiene/verify.mjs`) read this parameter off
62
+ * the rendered story and fail CLOSED when parameters cannot be read at all.
63
+ */
64
+ export type GuardrailSpecimenParameter = {
65
+ /** Warn codes this story may emit at rest — a closed set, not a wildcard. */
66
+ warns: DevWarnCode[];
67
+ /** Why the warned state is the story's subject (recorded per LC-57). */
68
+ reason: string;
69
+ };
70
+
35
71
  const seen = new Set<string>();
36
72
 
37
73
  function isDev(): boolean {
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatCurrency, formatNumber, formatPercent, resolveCurrencyCode } from "./numbers";
3
+
4
+ // Pin locale so assertions don't depend on the host machine.
5
+ const enUS = { locale: "en-US" } as const;
6
+
7
+ describe("formatNumber (LC-12 cardinal register)", () => {
8
+ it("groups thousands — the aggregate that used to render raw", () => {
9
+ expect(formatNumber(15400, enUS)).toBe("15,400");
10
+ expect(formatNumber(1234567, enUS)).toBe("1,234,567");
11
+ });
12
+
13
+ it("keeps up to two fraction digits without inventing any", () => {
14
+ expect(formatNumber(5133.33, enUS)).toBe("5,133.33");
15
+ expect(formatNumber(5133.3, enUS)).toBe("5,133.3");
16
+ expect(formatNumber(5133, enUS)).toBe("5,133");
17
+ expect(formatNumber(5133.336, enUS)).toBe("5,133.34");
18
+ });
19
+
20
+ it("pins fraction digits with precision (fixed-precision columns)", () => {
21
+ expect(formatNumber(15400, { ...enUS, precision: 2 })).toBe("15,400.00");
22
+ expect(formatNumber(15400.6, { ...enUS, precision: 0 })).toBe("15,401");
23
+ });
24
+
25
+ it("honours a wider maximum for report floats", () => {
26
+ expect(formatNumber(3.14159, { ...enUS, maximumFractionDigits: 4 })).toBe("3.1416");
27
+ });
28
+
29
+ it("can drop grouping when a caller owns the layout", () => {
30
+ expect(formatNumber(15400, { ...enUS, grouping: false })).toBe("15400");
31
+ });
32
+
33
+ it("coerces numeric strings — wire values still reach the register", () => {
34
+ expect(formatNumber("15400", enUS)).toBe("15,400");
35
+ });
36
+
37
+ it("renders empty input as empty and passes unparseable text through", () => {
38
+ expect(formatNumber(null, enUS)).toBe("");
39
+ expect(formatNumber(undefined, enUS)).toBe("");
40
+ expect(formatNumber("", enUS)).toBe("");
41
+ expect(formatNumber("n/a", enUS)).toBe("n/a");
42
+ expect(formatNumber(Number.NaN, enUS)).toBe("");
43
+ });
44
+ });
45
+
46
+ describe("formatCurrency (LC-12 money register)", () => {
47
+ it("defaults to two fraction digits so a money column stays aligned", () => {
48
+ expect(formatCurrency(15400, enUS)).toBe("15,400.00");
49
+ expect(formatCurrency(15400, { ...enUS, symbol: "$" })).toBe("$15,400.00");
50
+ });
51
+
52
+ it("elides a whole-value .00 in the summary-tile variant", () => {
53
+ expect(formatCurrency(48250, { ...enUS, symbol: "$", fractionDigits: "auto" })).toBe("$48,250");
54
+ });
55
+
56
+ it("still shows cents in the summary variant when the value has them", () => {
57
+ expect(formatCurrency(48250.5, { ...enUS, symbol: "$", fractionDigits: "auto" })).toBe(
58
+ "$48,250.50",
59
+ );
60
+ });
61
+
62
+ it("renders an ISO code through the currency's own symbol placement", () => {
63
+ expect(formatCurrency(15400, { ...enUS, currency: "USD" })).toBe("$15,400.00");
64
+ expect(formatCurrency(15400, { ...enUS, currency: "EUR" })).toBe("€15,400.00");
65
+ });
66
+
67
+ it("resolves a known symbol to its code, prefixes an unknown one", () => {
68
+ expect(formatCurrency(1500, { ...enUS, symbol: "₹" })).toBe("₹1,500.00");
69
+ expect(formatCurrency(1500, { ...enUS, symbol: "¤" })).toBe("¤1,500.00");
70
+ });
71
+
72
+ it("keeps the grouped register when a bad code makes Intl throw", () => {
73
+ // A Frappe field can hand over a malformed code; the money register must
74
+ // still group ("EUROS1,500.00", never a bare "1500").
75
+ expect(formatCurrency(1500, { ...enUS, currency: "EUROS" })).toBe("EUROS1,500.00");
76
+ });
77
+
78
+ it("honours an explicit precision from a Frappe field", () => {
79
+ expect(formatCurrency(1500.456, { ...enUS, symbol: "$", precision: 3 })).toBe("$1,500.456");
80
+ });
81
+
82
+ it("renders empty input as empty", () => {
83
+ expect(formatCurrency(null, enUS)).toBe("");
84
+ expect(formatCurrency("", { ...enUS, symbol: "$" })).toBe("");
85
+ });
86
+ });
87
+
88
+ describe("formatPercent (LC-12 rate register)", () => {
89
+ it("takes percent units, not a ratio", () => {
90
+ expect(formatPercent(94.2, enUS)).toBe("94.2%");
91
+ expect(formatPercent(3, enUS)).toBe("3%");
92
+ });
93
+
94
+ it("pins report precision", () => {
95
+ expect(formatPercent(94.25, { ...enUS, precision: 1 })).toBe("94.3%");
96
+ });
97
+
98
+ it("renders empty input as empty", () => {
99
+ expect(formatPercent(null, enUS)).toBe("");
100
+ });
101
+ });
102
+
103
+ describe("resolveCurrencyCode", () => {
104
+ it("maps the common symbols and passes codes through", () => {
105
+ expect(resolveCurrencyCode("$")).toBe("USD");
106
+ expect(resolveCurrencyCode("₹")).toBe("INR");
107
+ expect(resolveCurrencyCode("eur")).toBe("EUR");
108
+ expect(resolveCurrencyCode("R$")).toBe("BRL");
109
+ expect(resolveCurrencyCode("¤")).toBeUndefined();
110
+ expect(resolveCurrencyCode(undefined)).toBeUndefined();
111
+ });
112
+ });
package/src/numbers.ts CHANGED
@@ -62,6 +62,8 @@ const CURRENCY_SYMBOL_TO_CODE: Record<string, string> = {
62
62
  "¥": "JPY",
63
63
  "₹": "INR",
64
64
  "₩": "KRW",
65
+ "₽": "RUB",
66
+ R$: "BRL",
65
67
  };
66
68
 
67
69
  /**
@@ -97,14 +99,22 @@ function resolveDigits(
97
99
  return { minimumFractionDigits: min, maximumFractionDigits: Math.max(min, max) };
98
100
  }
99
101
 
100
- function formatWith(num: number, options: Intl.NumberFormatOptions, locale?: string): string {
102
+ function tryFormatWith(
103
+ num: number,
104
+ options: Intl.NumberFormatOptions,
105
+ locale?: string,
106
+ ): string | null {
101
107
  try {
102
108
  return new Intl.NumberFormat(locale, options).format(num);
103
109
  } catch {
104
- return String(num);
110
+ return null;
105
111
  }
106
112
  }
107
113
 
114
+ function formatWith(num: number, options: Intl.NumberFormatOptions, locale?: string): string {
115
+ return tryFormatWith(num, options, locale) ?? String(num);
116
+ }
117
+
108
118
  /**
109
119
  * Cardinal / aggregate register — grouped digits, at most two fraction
110
120
  * digits ("15,400", "5,133.33"). Empty input renders "" and an unparseable
@@ -142,14 +152,19 @@ export function formatCurrency(value: NumberInput, options: CurrencyFormatOption
142
152
  const code = currency ?? resolveCurrencyCode(symbol);
143
153
  const useGrouping = grouping ?? true;
144
154
  if (code) {
145
- return formatWith(
155
+ // A malformed code from a Frappe field must not cost the whole money
156
+ // register: fall back to the grouped register with the label prefixed
157
+ // rather than letting Intl's throw leak a bare `String(num)`.
158
+ const styled = tryFormatWith(
146
159
  coerced.num,
147
160
  { ...digits, useGrouping, style: "currency", currency: code },
148
161
  locale,
149
162
  );
163
+ if (styled != null) return styled;
150
164
  }
151
165
  const amount = formatWith(coerced.num, { ...digits, useGrouping }, locale);
152
- return symbol ? `${symbol}${amount}` : amount;
166
+ const label = symbol ?? currency;
167
+ return label ? `${label}${amount}` : amount;
153
168
  }
154
169
 
155
170
  /**
@@ -104,6 +104,85 @@ describe("readOverridesCookie", () => {
104
104
  });
105
105
  });
106
106
 
107
+ // Regression: react-cookie's jar (universal-cookie) auto-JSON-parses every
108
+ // cookie value it can, so the KnobBridge/useTheme read path receives
109
+ // already-parsed OBJECTS — the old reader joined them into "[object Object]",
110
+ // JSON.parse threw, and overrides silently resolved to {} app-wide.
111
+ describe("readOverridesCookie with universal-cookie auto-parsed values", () => {
112
+ it("accepts an already-parsed object cookie value (universal-cookie shape)", () => {
113
+ const overrides: Partial<Knobs> = { borderRadius: "none", space: "small" };
114
+ expect(readOverridesCookie({ "mp.ov.0": overrides })).toEqual(overrides);
115
+ });
116
+
117
+ it("accepts a raw JSON string cookie value (document.cookie / SSR shape)", () => {
118
+ const overrides: Partial<Knobs> = { borderRadius: "none", space: "small" };
119
+ expect(readOverridesCookie({ "mp.ov.0": JSON.stringify(overrides) })).toEqual(overrides);
120
+ });
121
+
122
+ it("preserves nested interaction-state overrides from a parsed object", () => {
123
+ const overrides: Partial<Knobs> = { hover: { elevation: "large" } };
124
+ expect(readOverridesCookie({ "mp.ov.0": overrides })).toEqual(overrides);
125
+ });
126
+
127
+ it("round-trips writeOverridesCookie output through an auto-parsing jar", () => {
128
+ const overrides: Partial<Knobs> = { density: "compact", fillStyle: "outlined" };
129
+ const { toSet } = writeOverridesCookie(overrides);
130
+ // Simulate universal-cookie readCookie: JSON.parse succeeds on the
131
+ // single-chunk value, so the jar hands back an object.
132
+ const jar = Object.fromEntries(toSet.map(({ key, value }) => [key, JSON.parse(value)]));
133
+ expect(readOverridesCookie(jar)).toEqual(overrides);
134
+ });
135
+
136
+ it("round-trips oversized multi-chunk payloads through an auto-parsing jar", () => {
137
+ const bigValue = "x".repeat(8000);
138
+ const overrides = { animation: bigValue } as unknown as Partial<Knobs>;
139
+ const { toSet } = writeOverridesCookie(overrides);
140
+ expect(toSet.length).toBeGreaterThan(1);
141
+ // universal-cookie leaves each chunk a string: a JSON fragment throws in
142
+ // its JSON.parse attempt, so readCookie falls back to the raw value.
143
+ const jar = Object.fromEntries(
144
+ toSet.map(({ key, value }) => {
145
+ try {
146
+ return [key, JSON.parse(value)];
147
+ } catch {
148
+ return [key, value];
149
+ }
150
+ }),
151
+ );
152
+ expect(readOverridesCookie(jar)).toEqual(overrides);
153
+ });
154
+
155
+ it("returns safe empty for garbage values", () => {
156
+ expect(readOverridesCookie({ "mp.ov.0": 42 })).toEqual({});
157
+ expect(readOverridesCookie({ "mp.ov.0": true })).toEqual({});
158
+ expect(readOverridesCookie({ "mp.ov.0": ["not", "overrides"] })).toEqual({});
159
+ expect(readOverridesCookie({ "mp.ov.0": "not{valid json" })).toEqual({});
160
+ expect(readOverridesCookie({ "mp.ov.0": "[object Object]" })).toEqual({});
161
+ });
162
+
163
+ it("returns safe empty for mixed chunk types (object followed by string)", () => {
164
+ expect(
165
+ readOverridesCookie({
166
+ "mp.ov.0": { borderRadius: "none" },
167
+ "mp.ov.1": '{"space":"small"}',
168
+ }),
169
+ ).toEqual({});
170
+ });
171
+
172
+ it("drops non-knob-shaped entries inside a parsed object", () => {
173
+ expect(
174
+ readOverridesCookie({
175
+ "mp.ov.0": {
176
+ borderRadius: "none",
177
+ count: 3,
178
+ list: ["a"],
179
+ hover: { elevation: "large", depth: { nested: true } },
180
+ },
181
+ }),
182
+ ).toEqual({ borderRadius: "none", hover: { elevation: "large" } });
183
+ });
184
+ });
185
+
107
186
  describe("readPresetCookie", () => {
108
187
  it("returns undefined when cookie is missing", () => {
109
188
  expect(readPresetCookie({})).toBeUndefined();
@@ -116,6 +195,11 @@ describe("readPresetCookie", () => {
116
195
  it("returns the preset name", () => {
117
196
  expect(readPresetCookie({ [cookiePreset]: "dark-mode" })).toBe("dark-mode");
118
197
  });
198
+
199
+ it("returns undefined for non-string jar values (universal-cookie auto-parse)", () => {
200
+ expect(readPresetCookie({ [cookiePreset]: 123 })).toBeUndefined();
201
+ expect(readPresetCookie({ [cookiePreset]: { nested: true } })).toBeUndefined();
202
+ });
119
203
  });
120
204
 
121
205
  describe("writePresetCookie", () => {
@@ -69,34 +69,82 @@ function chunkKey(index: number): string {
69
69
  // Cookie read helpers (work with raw cookie objects from react-cookie)
70
70
  // ---------------------------------------------------------------------------
71
71
 
72
+ /**
73
+ * Shape-validate a parsed overrides payload before it reaches knob
74
+ * resolution: keep knob-shaped entries — string values plus one-level
75
+ * StateKnobs-style objects of string values (`hover`, `press`, …) — and drop
76
+ * everything else, so a corrupt or foreign cookie can never spread garbage
77
+ * (arrays, numbers, deep objects) into `{ ...preset.knobs, ...overrides }`.
78
+ */
79
+ function sanitizeOverrides(parsed: unknown): Partial<Knobs> {
80
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
81
+ return {};
82
+ }
83
+ const overrides: Record<string, unknown> = {};
84
+ for (const [key, value] of Object.entries(parsed)) {
85
+ if (typeof value === "string") {
86
+ overrides[key] = value;
87
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
88
+ const nested: Record<string, string> = {};
89
+ for (const [stateKey, stateValue] of Object.entries(value)) {
90
+ if (typeof stateValue === "string") nested[stateKey] = stateValue;
91
+ }
92
+ overrides[key] = nested;
93
+ }
94
+ }
95
+ return overrides as Partial<Knobs>;
96
+ }
97
+
72
98
  /**
73
99
  * Read the overrides cookie value, handling chunked cookies.
74
- * Accepts the cookies object from `useCookies()`.
100
+ *
101
+ * Accepts BOTH value shapes a cookie jar can deliver:
102
+ * - already-parsed OBJECTS — react-cookie's `useCookies()` jar
103
+ * (universal-cookie) auto-JSON-parses every cookie value it can (its
104
+ * `readCookie` tries `JSON.parse` unless `doNotParse` is set), so a
105
+ * single-chunk overrides cookie arrives here as an object, not a string;
106
+ * - raw JSON STRINGS — direct `document.cookie` / SSR cookie-header reads
107
+ * (`loadPresetOverrides`), and multi-chunk payloads on every path (a
108
+ * fragment of a split JSON document does not parse on its own, so even
109
+ * universal-cookie leaves those chunks as strings).
110
+ *
111
+ * Anything else (numbers, arrays, mixed chunk types, malformed JSON) reads
112
+ * as no overrides, and every parsed payload is shape-validated before use.
75
113
  */
76
- export function readOverridesCookie(cookies: Record<string, string | undefined>): Partial<Knobs> {
114
+ export function readOverridesCookie(cookies: Record<string, unknown>): Partial<Knobs> {
77
115
  // Reconstruct from chunked cookies: mp.ov.0, mp.ov.1, …
78
- const chunks: string[] = [];
116
+ const chunks: unknown[] = [];
79
117
  for (let i = 0; i < maxChunks; i++) {
80
118
  const chunk = cookies[chunkKey(i)];
81
119
  if (!chunk) break;
82
120
  chunks.push(chunk);
83
121
  }
84
- if (chunks.length > 0) {
85
- try {
86
- return JSON.parse(joinSplitCookieValues(chunks));
87
- } catch {
88
- return {};
89
- }
122
+ if (chunks.length === 0) return {};
123
+
124
+ // universal-cookie path: the jar already parsed the JSON for us.
125
+ if (chunks.length === 1 && typeof chunks[0] === "object") {
126
+ return sanitizeOverrides(chunks[0]);
90
127
  }
91
128
 
92
- return {};
129
+ // Raw string path: join chunks and parse the canonical JSON serialization.
130
+ if (!chunks.every((chunk): chunk is string => typeof chunk === "string")) {
131
+ return {};
132
+ }
133
+ try {
134
+ return sanitizeOverrides(JSON.parse(joinSplitCookieValues(chunks)));
135
+ } catch {
136
+ return {};
137
+ }
93
138
  }
94
139
 
95
140
  /**
96
- * Read the preset name from the cookie.
141
+ * Read the preset name from the cookie. universal-cookie auto-JSON-parses
142
+ * jar values, so a numeric-looking name would arrive as a number — preset
143
+ * names are strings by contract, anything else reads as no preset.
97
144
  */
98
- export function readPresetCookie(cookies: Record<string, string | undefined>): string | undefined {
99
- return cookies[cookiePreset] || undefined;
145
+ export function readPresetCookie(cookies: Record<string, unknown>): string | undefined {
146
+ const value = cookies[cookiePreset];
147
+ return typeof value === "string" && value.length > 0 ? value : undefined;
100
148
  }
101
149
 
102
150
  // ---------------------------------------------------------------------------
@@ -18,6 +18,7 @@ export * from "./Preset";
18
18
  export type { Preset as PresetDefinition, IntentOverride } from "./preset.types";
19
19
  export * from "./PresetContext";
20
20
  export * from "./presets";
21
+ export * from "./radiusClass";
21
22
  export * from "./readableColor";
22
23
  export * from "./recipes";
23
24
  export * from "./resolveKnobs";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Radius-class declarations (D-09 / LC-60).
3
+ *
4
+ * Most parts prove they obey the radius knob by simply going square at
5
+ * `borderRadius:none`. A few legitimately do not — an ornamental dot has no
6
+ * arc for the knob to move (R-PILL), and an avatar's circle is the object's
7
+ * identity rather than a style (R-IDENTITY). Those parts must SAY SO on the
8
+ * node, because the alternative — a probe that skips anything that looks
9
+ * round — silently exempts real KNOB-TOTALITY defects (LC-18).
10
+ *
11
+ * The declaration is only half the contract: the `radius:none` sweep
12
+ * validates it against the registry GENERATED from the spec's radius-class
13
+ * table (`scripts/radius-identity-registry.mjs`), so a class this file cannot
14
+ * spell, or an R-IDENTITY part the spec never adjudicated, still fails. This
15
+ * module is the writing end; `docs/theme-propagation-spec.md` is the registry.
16
+ */
17
+
18
+ /**
19
+ * The radius classes whose parts stay round at `borderRadius:none`.
20
+ *
21
+ * - `R-PILL` — a pill/circle at every radius value (ornamental dots, badge
22
+ * indicators, icon wells). Size still tracks the size knob.
23
+ * - `R-IDENTITY` — the shape IS the part's identity (Avatar, brand marks).
24
+ * Membership is enumerated in the spec table and enforced per part.
25
+ */
26
+ export type KnobImmuneRadiusClass = "R-PILL" | "R-IDENTITY";
27
+
28
+ export interface RadiusClassDeclaration {
29
+ "data-radius-class": KnobImmuneRadiusClass;
30
+ "data-radius-part": string;
31
+ }
32
+
33
+ /**
34
+ * Declare a knob-immune shape so the `radius:none` sweep can license it.
35
+ *
36
+ * Spread onto the outermost node of the shape — a declaration covers its own
37
+ * anatomy, so inner painted parts (an avatar's image/fallback) inherit it.
38
+ *
39
+ * @param radiusClass the class the spec's Radius table assigns the part
40
+ * @param part the part name as the spec writes it (the R-IDENTITY registry
41
+ * is matched by this name)
42
+ *
43
+ * @example
44
+ * <Frame {...radiusClassProps("R-IDENTITY", "Avatar")} circular />
45
+ */
46
+ export function radiusClassProps(
47
+ radiusClass: KnobImmuneRadiusClass,
48
+ part: string,
49
+ ): RadiusClassDeclaration {
50
+ return { "data-radius-class": radiusClass, "data-radius-part": part };
51
+ }
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveAccentTintedSurface } from "./readableColor";
3
+ import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
4
+
5
+ // The stock accent ramp (defaults/accent.ts) as the theme resolves it.
6
+ const lightAccent = [
7
+ "hsla(250, 50%, 62%, 1)",
8
+ "hsla(250, 50%, 58%, 1)",
9
+ "hsla(250, 50%, 54%, 1)",
10
+ "hsla(250, 50%, 48%, 1)",
11
+ "hsla(250, 50%, 46%, 1)",
12
+ "hsla(250, 50%, 44%, 1)",
13
+ "hsla(250, 50%, 42%, 1)",
14
+ "hsla(250, 50%, 40%, 1)",
15
+ "hsla(250, 50%, 38%, 1)",
16
+ "hsla(250, 50%, 36%, 1)",
17
+ "hsla(250, 50%, 95%, 1)",
18
+ "hsla(250, 50%, 98%, 1)",
19
+ ];
20
+ const darkAccent = [
21
+ "hsla(250, 50%, 35%, 1)",
22
+ "hsla(250, 50%, 38%, 1)",
23
+ "hsla(250, 50%, 41%, 1)",
24
+ "hsla(250, 50%, 43%, 1)",
25
+ "hsla(250, 50%, 46%, 1)",
26
+ "hsla(250, 50%, 49%, 1)",
27
+ "hsla(250, 50%, 52%, 1)",
28
+ "hsla(250, 50%, 54%, 1)",
29
+ "hsla(250, 50%, 57%, 1)",
30
+ "hsla(250, 50%, 60%, 1)",
31
+ "hsla(250, 50%, 90%, 1)",
32
+ "hsla(250, 50%, 95%, 1)",
33
+ ];
34
+
35
+ const lightPaper = "#ffffff";
36
+ const lightInk = "#242226";
37
+ const darkPaper = "#161519";
38
+ const darkInk = "#f0eff1";
39
+
40
+ const lum = (raw: string) => relativeLuminance(normalizeToHex(raw)!);
41
+
42
+ describe("resolveAccentTintedSurface", () => {
43
+ it("takes the ramp's pale surface end in a light scheme", () => {
44
+ const tint = resolveAccentTintedSurface({
45
+ steps: lightAccent,
46
+ paper: lightPaper,
47
+ ink: lightInk,
48
+ });
49
+ expect(tint).toBe(lightAccent[10]);
50
+ });
51
+
52
+ it("takes the ramp's deep surface end in a dark scheme", () => {
53
+ const tint = resolveAccentTintedSurface({ steps: darkAccent, paper: darkPaper, ink: darkInk });
54
+ expect(tint).toBe(darkAccent[0]);
55
+ });
56
+
57
+ // The bug this rule replaced: keying the pick off a SURFACE token let a tint
58
+ // sub-theme (which re-ramps surfaces) slide the chip a step per tint. Scheme
59
+ // polarity comes from the paper/ink anchors, which a tint leaves alone.
60
+ it("does not drift when surrounding surfaces are re-ramped by a tint", () => {
61
+ const untinted = resolveAccentTintedSurface({
62
+ steps: lightAccent,
63
+ paper: lightPaper,
64
+ ink: lightInk,
65
+ });
66
+ for (const tintedPaper of ["#fffce8", "#fff8f8", "#f2fbf5"]) {
67
+ expect(
68
+ resolveAccentTintedSurface({ steps: lightAccent, paper: tintedPaper, ink: lightInk }),
69
+ ).toBe(untinted);
70
+ }
71
+ });
72
+
73
+ it("stays a SURFACE tier — never the solid an accent CTA takes", () => {
74
+ // The solid is accentBackground (= the ramp's mid, palette-shape solid).
75
+ const lightSolid = lightAccent[2];
76
+ const darkSolid = darkAccent[9];
77
+ expect(
78
+ resolveAccentTintedSurface({ steps: lightAccent, paper: lightPaper, ink: lightInk }),
79
+ ).not.toBe(lightSolid);
80
+ expect(
81
+ resolveAccentTintedSurface({ steps: darkAccent, paper: darkPaper, ink: darkInk }),
82
+ ).not.toBe(darkSolid);
83
+ });
84
+
85
+ it("keeps a luminance-picked label readable on the tint in both schemes", () => {
86
+ const cases = [
87
+ { steps: lightAccent, paper: lightPaper, ink: lightInk },
88
+ { steps: darkAccent, paper: darkPaper, ink: darkInk },
89
+ ];
90
+ for (const c of cases) {
91
+ const tint = resolveAccentTintedSurface(c)!;
92
+ const best = Math.max(
93
+ contrastRatio(lum(tint), lum(c.paper)),
94
+ contrastRatio(lum(tint), lum(c.ink)),
95
+ );
96
+ expect(best).toBeGreaterThanOrEqual(4.5);
97
+ }
98
+ });
99
+
100
+ it("returns undefined when the accent ramp or an anchor is missing", () => {
101
+ expect(
102
+ resolveAccentTintedSurface({ steps: [], paper: lightPaper, ink: lightInk }),
103
+ ).toBeUndefined();
104
+ expect(
105
+ resolveAccentTintedSurface({ steps: lightAccent, paper: undefined, ink: lightInk }),
106
+ ).toBeUndefined();
107
+ expect(
108
+ resolveAccentTintedSurface({
109
+ steps: Array.from({ length: 12 }, () => "var(--nope)"),
110
+ paper: lightPaper,
111
+ ink: lightInk,
112
+ }),
113
+ ).toBeUndefined();
114
+ });
115
+ });
@@ -102,41 +102,62 @@ export function useAccentOnSurface(): string {
102
102
  }
103
103
 
104
104
  /**
105
- * Accent-TINTED surface step — the accent-ramp analogue of a neutral surface
106
- * tier, for selected/current chips that carry the accent language at surface
107
- * strength rather than as a solid fill (LC-05: selected state is accent; the
108
- * ViewSwitcher rider pins segmented-control strength at "tinted", not solid).
105
+ * Accent SURFACE tier of the accent ramp, mirrored by scheme — the fill for a
106
+ * selected/current chip that must carry the accent language at SURFACE
107
+ * strength rather than as a solid (LC-05: selection is accent; the ViewSwitcher
108
+ * rider pins segmented-control strength at "tinted", not the solid an accent
109
+ * CTA takes).
109
110
  *
110
- * Picks the accent step whose relative luminance sits closest to the neutral
111
- * tier it replaces, so the chip stays a surface in BOTH schemes without a
112
- * per-component light/dark fork: against this repo's stock accent ramp that
113
- * resolves to a pale violet in light and a deep violet in dark, because the
114
- * accent ramp flips with the scheme exactly as the neutral ramp does.
111
+ * The step is a declared ramp POSITION, the same way the neutral ramp declares
112
+ * its tiers (surface `$color2`, UI background `$color3`-`$color5`, text
113
+ * `$color11`-`$color12`): the accent ramp's surface end is its palest step in a
114
+ * light scheme and its deepest in a dark one, so the chip reads as a tinted
115
+ * surface on either page. Scheme polarity comes from the paper/ink ANCHORS,
116
+ * never from a surface token — a tint re-ramps surfaces, and keying off one
117
+ * made this pick drift a step per tint (measured: the chip slid from the pale
118
+ * accent to the palest accent under a yellow tint).
115
119
  *
116
- * Returns `undefined` when the accent ramp is absent or unparseable — callers
117
- * keep their neutral default rather than render an unthemed chip.
120
+ * Returns `undefined` when the accent ramp is absent or unparseable (tint
121
+ * sub-themes without an accent ramp) — callers keep their neutral default
122
+ * rather than render an unthemed chip.
118
123
  */
119
- export function useAccentTintedSurface(neutralToken = "$color4"): string | undefined {
124
+ // Ramp positions (1-based) of the accent surface tier per scheme.
125
+ const ACCENT_SURFACE_STEP_LIGHT = 11;
126
+ const ACCENT_SURFACE_STEP_DARK = 1;
127
+
128
+ export interface ResolveAccentTintedSurfaceInput {
129
+ /** Accent ramp steps 1..12, as raw color values; holes allowed. */
130
+ steps: (string | undefined)[];
131
+ /** Scheme paper anchor (`$color1`). */
132
+ paper?: string;
133
+ /** Scheme ink anchor (`$color12`). */
134
+ ink?: string;
135
+ }
136
+
137
+ /** Pure resolver behind {@link useAccentTintedSurface} — see its docs. */
138
+ export function resolveAccentTintedSurface({
139
+ steps,
140
+ paper,
141
+ ink,
142
+ }: ResolveAccentTintedSurfaceInput): string | undefined {
143
+ const paperHex = paper ? normalizeToHex(paper) : null;
144
+ const inkHex = ink ? normalizeToHex(ink) : null;
145
+ if (!paperHex || !inkHex) return undefined;
146
+ const isLight = relativeLuminance(paperHex) > relativeLuminance(inkHex);
147
+ const step = steps[(isLight ? ACCENT_SURFACE_STEP_LIGHT : ACCENT_SURFACE_STEP_DARK) - 1];
148
+ if (!step || !normalizeToHex(step)) return undefined;
149
+ return step;
150
+ }
151
+
152
+ export function useAccentTintedSurface(): string | undefined {
120
153
  const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
121
- const neutral = resolveTokenValue(theme, neutralToken);
154
+ const paper = resolveTokenValue(theme, "color1");
155
+ const ink = resolveTokenValue(theme, "color12");
122
156
  const steps: (string | undefined)[] = [];
123
157
  for (let i = 1; i <= 12; i++) steps.push(resolveTokenValue(theme, `accent${i}`));
124
158
  // eslint-disable-next-line react-hooks/exhaustive-deps
125
- return useMemo(() => {
126
- const neutralHex = neutral ? normalizeToHex(neutral) : null;
127
- if (!neutralHex) return undefined;
128
- const target = relativeLuminance(neutralHex);
129
- let best: string | undefined;
130
- let bestDelta = Number.POSITIVE_INFINITY;
131
- for (const step of steps) {
132
- const hex = step ? normalizeToHex(step) : null;
133
- if (!step || !hex) continue;
134
- const delta = Math.abs(relativeLuminance(hex) - target);
135
- if (delta < bestDelta) {
136
- bestDelta = delta;
137
- best = step;
138
- }
139
- }
140
- return best;
141
- }, [neutral, steps.join("|")]);
159
+ return useMemo(
160
+ () => resolveAccentTintedSurface({ steps, paper, ink }),
161
+ [paper, ink, steps.join("|")],
162
+ );
142
163
  }
@@ -18,8 +18,13 @@ import {
18
18
  import type { Knobs } from "./knobs";
19
19
  import type { KnobProps } from "./recipes";
20
20
  import { cornerSmoothClassName } from "./cornerSmoothing";
21
- import { defaultPreset, boldPreset } from "./presets";
22
- import { applyDensity, capContainerRadius, resolveKnobs } from "./resolveKnobs";
21
+ import { defaultPreset, boldPreset, heroPreset } from "./presets";
22
+ import {
23
+ applyDensity,
24
+ capContainerRadius,
25
+ resolveKnobs,
26
+ resolvePageTitleScale,
27
+ } from "./resolveKnobs";
23
28
  import {
24
29
  normalizeToHex,
25
30
  relativeLuminance,
@@ -47,6 +52,7 @@ const expectedKnobPropKeys: (keyof KnobProps)[] = [
47
52
  "gapLg",
48
53
  "sizeToken",
49
54
  "heading",
55
+ "pageTitle",
50
56
  "body",
51
57
  "textWeight",
52
58
  "transition",
@@ -731,6 +737,44 @@ describe("resolveKnobs", () => {
731
737
  });
732
738
  });
733
739
 
740
+ // ── pageTitleScale (D-11 hero-H1 dial) ──────────────────────
741
+
742
+ describe("pageTitleScale knob", () => {
743
+ it("defaults to the moderate page-title step ($8 = 32px)", () => {
744
+ expect(resolveKnobs(defaultKnobs).knobProps.pageTitle).toEqual({ size: "$8" });
745
+ });
746
+
747
+ it('"display" re-opens the hero step ($10 = 64px)', () => {
748
+ expect(resolveKnobs(knobsWith({ pageTitleScale: "display" })).knobProps.pageTitle).toEqual({
749
+ size: "$10",
750
+ });
751
+ });
752
+
753
+ it("an omitted knob still resolves moderate (preset constructions predate the dial)", () => {
754
+ const { pageTitleScale: _omitted, ...withoutDial } = defaultKnobs;
755
+ expect(resolveKnobs(withoutDial as Knobs).knobProps.pageTitle).toEqual({ size: "$8" });
756
+ });
757
+
758
+ it("resolvePageTitleScale is the one table both channels read", () => {
759
+ expect(resolvePageTitleScale("moderate")).toEqual({ size: "$8" });
760
+ expect(resolvePageTitleScale("display")).toEqual({ size: "$10" });
761
+ });
762
+
763
+ it("product presets ship moderate; `hero` is the only display opt-in", () => {
764
+ expect(defaultPreset.knobs.pageTitleScale).toBe("moderate");
765
+ expect(boldPreset.knobs.pageTitleScale).toBe("moderate");
766
+ expect(heroPreset.knobs.pageTitleScale).toBe("display");
767
+ expect(resolveKnobs(heroPreset.knobs).knobProps.pageTitle).toEqual({ size: "$10" });
768
+ });
769
+
770
+ it("`hero` differs from the default preset in nothing but the title step", () => {
771
+ const { pageTitleScale: _hero, ...heroRest } = heroPreset.knobs;
772
+ const { pageTitleScale: _default, ...defaultRest } = defaultPreset.knobs;
773
+ expect(heroRest).toEqual(defaultRest);
774
+ expect(heroPreset.theme).toBe(defaultPreset.theme);
775
+ });
776
+ });
777
+
734
778
  // ── animation ─────────────────────────────────────────────
735
779
 
736
780
  describe("animation knob", () => {
package/types/dates.d.ts CHANGED
@@ -38,6 +38,24 @@ export interface AbsoluteDateFormatOptions {
38
38
  }
39
39
  /** ISO date-only shape ("YYYY-MM-DD"). */
40
40
  export declare const ISO_DATE_ONLY: RegExp;
41
+ /**
42
+ * Parse a date value onto the LOCAL calendar day it names — the canonical
43
+ * parse for anything that will be positioned on a day grid (calendar cells,
44
+ * Gantt bars, date axes) rather than merely formatted.
45
+ *
46
+ * A date-only string ("YYYY-MM-DD") is a calendar date carrying no timezone,
47
+ * which is how Frappe sends `Date` fields. ECMA-262 parses that form as UTC
48
+ * midnight, so every zone west of UTC reads it as the *previous* local day,
49
+ * and no zone but UTC lands it on the local midnight that day-grid math tests
50
+ * for. Those are built from local parts instead.
51
+ *
52
+ * Values that carry a time ("YYYY-MM-DD HH:MM:SS", ISO with an offset, epoch
53
+ * millis) are genuine instants and keep the timezone-aware parse untouched.
54
+ *
55
+ * Returns null for empty and unparseable input, and for out-of-range parts
56
+ * ("2026-13-45") rather than silently rolling them into the next month.
57
+ */
58
+ export declare function parseCalendarDate(value: DateInput): Date | null;
41
59
  /**
42
60
  * Compact absolute date — the house register for every absolute date.
43
61
  * "Jul 1" / "Jul 1, 2025" (year auto-elided when current), or with
@@ -1 +1 @@
1
- {"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../src/dates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACnC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,0CAA0C;AAC1C,eAAO,MAAM,aAAa,QAAwB,CAAC;AAyBnD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,yBAA8B,GACtC,MAAM,CA0BR;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,IAAI,CAAC,yBAAyB,EAAE,UAAU,CAAM,GACxD,MAAM,CAER;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAgB1F;AAED,iFAAiF;AACjF,eAAO,MAAM,UAAU,QAAqC,CAAC;AAE7D,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAuB9F;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAa1F;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAW9F;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAU5F;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,EAAE,CAO3E;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAO,GAC1D,MAAM,EAAE,CAIV"}
1
+ {"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../src/dates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACnC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,0CAA0C;AAC1C,eAAO,MAAM,aAAa,QAAwB,CAAC;AAyBnD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAkB/D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,yBAA8B,GACtC,MAAM,CA0BR;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,IAAI,CAAC,yBAAyB,EAAE,UAAU,CAAM,GACxD,MAAM,CAER;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAgB1F;AAED,iFAAiF;AACjF,eAAO,MAAM,UAAU,QAAqC,CAAC;AAE7D,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAuB9F;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAa1F;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAW9F;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAU5F;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,EAAE,CAO3E;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAO,GAC1D,MAAM,EAAE,CAIV"}
@@ -16,6 +16,41 @@ export type DevWarnDetail = {
16
16
  region?: string;
17
17
  [key: string]: unknown;
18
18
  };
19
+ /**
20
+ * LC-57 GUARDRAIL-CLEAN-STORIES allowlist entry, declared as a Storybook story
21
+ * parameter (`parameters.guardrailSpecimen`).
22
+ *
23
+ * Catalog stories emit zero `[theme]` guardrail warnings at rest. A story whose
24
+ * whole purpose is to demonstrate the warned state (a disabled-dial specimen, a
25
+ * pager boundary case) is allowlisted by declaring itself:
26
+ *
27
+ * ```ts
28
+ * export const Disabled: Story = {
29
+ * parameters: {
30
+ * guardrailSpecimen: {
31
+ * warns: ["bare-disabled"],
32
+ * reason: "Demonstrates the disabled dial; Stepper has no disabledReason plumbing.",
33
+ * } satisfies GuardrailSpecimenParameter,
34
+ * },
35
+ * };
36
+ * ```
37
+ *
38
+ * Semantics, pinned:
39
+ * - `warns` is the CLOSED set of codes the story may emit at rest. An emitted
40
+ * code outside the set still FAILs the story.
41
+ * - A declared code that happens not to fire passes — component improvements
42
+ * must never break an allowlist entry.
43
+ * - The declaration NEVER suppresses the console warning — the warn still
44
+ * fires; only the warn-harvester tolerates it (explicit, never tacit).
45
+ * - Harvesters (`p-results/story-hygiene/verify.mjs`) read this parameter off
46
+ * the rendered story and fail CLOSED when parameters cannot be read at all.
47
+ */
48
+ export type GuardrailSpecimenParameter = {
49
+ /** Warn codes this story may emit at rest — a closed set, not a wildcard. */
50
+ warns: DevWarnCode[];
51
+ /** Why the warned state is the story's subject (recorded per LC-57). */
52
+ reason: string;
53
+ };
19
54
  /**
20
55
  * Emit a once-per-key console warning in non-production builds.
21
56
  */
@@ -1 +1 @@
1
- {"version":3,"file":"devWarn.d.ts","sourceRoot":"","sources":["../src/devWarn.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"devWarn.d.ts","sourceRoot":"","sources":["../src/devWarn.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,WAAW,GACnB,sBAAsB,GACtB,eAAe,GACf,mBAAmB,GACnB,eAAe,GACf,iBAAiB,GACjB,yBAAyB,GACzB,wBAAwB,GACxB,mBAAmB,GACnB,qBAAqB,GACrB,cAAc,GAGd,aAAa,CAAC;AAElB,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,6EAA6E;IAC7E,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAQF;;GAEG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,GAAE,aAAkB,GAAG,IAAI,CAwB3E;AAED,4BAA4B;AAC5B,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,6DAA6D;AAC7D,eAAO,MAAM,YAAY,gBAAU,CAAC;AACpC,eAAO,MAAM,uBAAuB,2BAAqB,CAAC;AAE1D;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,IAAI,CAWP;AAID,2DAA2D;AAC3D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,MAAM,GAAE,aAAkB,GACzB,IAAI,CASN;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,IAAI,CAIP;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,IAAI,CAGP;AAED,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,IAAI,CAOP;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,IAAI,CASP;AAED,yCAAyC;AACzC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,IAAI,CASP;AAED,yDAAyD;AACzD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,IAAI,CAMP;AAED,KAAK,WAAW,GACZ;IACE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACD,IAAI,GACJ,SAAS,CAAC;AAEd;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,GAAE,aAAkB,GAAG,IAAI,CAMpF"}
@@ -1 +1 @@
1
- {"version":3,"file":"numbers.d.ts","sourceRoot":"","sources":["../src/numbers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sDAAsD;IACtD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CACpC;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAYvD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIlF;AAiCD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAQ1F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAyB9F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAK5F"}
1
+ {"version":3,"file":"numbers.d.ts","sourceRoot":"","sources":["../src/numbers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sDAAsD;IACtD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CACpC;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAcvD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIlF;AAyCD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAQ1F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CA8B9F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAK5F"}
@@ -55,7 +55,7 @@ export interface ButtonColorConfig extends ComponentColorConfig {
55
55
  }
56
56
  export declare const buttonColors: ButtonColorConfig;
57
57
  export declare const inputColors: ComponentColorConfig;
58
- export declare function $color(step: ColorStep): "$color1" | "$color12" | "$color2" | "$color7" | "$color4" | "$color5" | "$color11" | "$color3" | "$color6" | "$color8" | "$color9" | "$color10";
58
+ export declare function $color(step: ColorStep): "$color3" | "$color1" | "$color2" | "$color12" | "$color7" | "$color4" | "$color5" | "$color11" | "$color6" | "$color8" | "$color9" | "$color10";
59
59
  export interface SemanticGroup {
60
60
  key: string;
61
61
  label: string;
@@ -26,13 +26,27 @@ export declare function splitCookieValue(value: string): string[];
26
26
  export declare function joinSplitCookieValues(chunks: string[]): string;
27
27
  /**
28
28
  * Read the overrides cookie value, handling chunked cookies.
29
- * Accepts the cookies object from `useCookies()`.
29
+ *
30
+ * Accepts BOTH value shapes a cookie jar can deliver:
31
+ * - already-parsed OBJECTS — react-cookie's `useCookies()` jar
32
+ * (universal-cookie) auto-JSON-parses every cookie value it can (its
33
+ * `readCookie` tries `JSON.parse` unless `doNotParse` is set), so a
34
+ * single-chunk overrides cookie arrives here as an object, not a string;
35
+ * - raw JSON STRINGS — direct `document.cookie` / SSR cookie-header reads
36
+ * (`loadPresetOverrides`), and multi-chunk payloads on every path (a
37
+ * fragment of a split JSON document does not parse on its own, so even
38
+ * universal-cookie leaves those chunks as strings).
39
+ *
40
+ * Anything else (numbers, arrays, mixed chunk types, malformed JSON) reads
41
+ * as no overrides, and every parsed payload is shape-validated before use.
30
42
  */
31
- export declare function readOverridesCookie(cookies: Record<string, string | undefined>): Partial<Knobs>;
43
+ export declare function readOverridesCookie(cookies: Record<string, unknown>): Partial<Knobs>;
32
44
  /**
33
- * Read the preset name from the cookie.
45
+ * Read the preset name from the cookie. universal-cookie auto-JSON-parses
46
+ * jar values, so a numeric-looking name would arrive as a number — preset
47
+ * names are strings by contract, anything else reads as no preset.
34
48
  */
35
- export declare function readPresetCookie(cookies: Record<string, string | undefined>): string | undefined;
49
+ export declare function readPresetCookie(cookies: Record<string, unknown>): string | undefined;
36
50
  /**
37
51
  * Write the preset name cookie.
38
52
  * Returns the cookie key and value for the caller to pass to setCookie.
@@ -1 +1 @@
1
- {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/theme/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAMrC,6CAA6C;AAC7C,eAAO,MAAM,YAAY,cAAc,CAAC;AAExC,sDAAsD;AACtD,eAAO,MAAM,qBAAqB,UAAU,CAAC;AAY7C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CASxD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAE9D;AAeD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAiB/F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,CAEhG;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAEpF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;IAC/D,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7C,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAgBA;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAQ5E;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAM7C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAQtE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC9C,OAAO,CAAC,KAAK,CAAC,CAGhB"}
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/theme/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAMrC,6CAA6C;AAC7C,eAAO,MAAM,YAAY,cAAc,CAAC;AAExC,sDAAsD;AACtD,eAAO,MAAM,qBAAqB,UAAU,CAAC;AAY7C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CASxD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAE9D;AAyCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAwBpF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAGrF;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAEpF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;IAC/D,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7C,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAgBA;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAQ5E;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAM7C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAQtE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC9C,OAAO,CAAC,KAAK,CAAC,CAGhB"}
@@ -2,6 +2,6 @@
2
2
  export declare function interTrackingEm(sizePx: number): number;
3
3
  /** Inter dynamic tracking for a rendered `sizePx`, in px (2-decimal). */
4
4
  export declare function interTrackingPx(sizePx: number): number;
5
- export declare const defaultHeadingFont: import("@tamagui/web").FillInFont<import("tamagui").GenericFont, 1 | 12 | 2 | 7 | 4 | 5 | 11 | 13 | 14 | 15 | 16 | 3 | 6 | 8 | 9 | 10 | "true">;
6
- export declare const defaultBodyFont: import("@tamagui/web").FillInFont<import("tamagui").GenericFont, 1 | 12 | 2 | 7 | 4 | 5 | 11 | 13 | 14 | 15 | 16 | 3 | 6 | 8 | 9 | 10 | "true">;
5
+ export declare const defaultHeadingFont: import("@tamagui/web").FillInFont<import("tamagui").GenericFont, 3 | 1 | 2 | 12 | 7 | 4 | 5 | 11 | 13 | 14 | 15 | 16 | 6 | 8 | 9 | 10 | "true">;
6
+ export declare const defaultBodyFont: import("@tamagui/web").FillInFont<import("tamagui").GenericFont, 3 | 1 | 2 | 12 | 7 | 4 | 5 | 11 | 13 | 14 | 15 | 16 | 6 | 8 | 9 | 10 | "true">;
7
7
  //# sourceMappingURL=fonts.d.ts.map
@@ -18,6 +18,7 @@ export * from "./Preset";
18
18
  export type { Preset as PresetDefinition, IntentOverride } from "./preset.types";
19
19
  export * from "./PresetContext";
20
20
  export * from "./presets";
21
+ export * from "./radiusClass";
21
22
  export * from "./readableColor";
22
23
  export * from "./recipes";
23
24
  export * from "./resolveKnobs";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,YAAY,EAAE,MAAM,IAAI,gBAAgB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACjF,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AAEzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,YAAY,EAAE,MAAM,IAAI,gBAAgB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACjF,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AAEzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Radius-class declarations (D-09 / LC-60).
3
+ *
4
+ * Most parts prove they obey the radius knob by simply going square at
5
+ * `borderRadius:none`. A few legitimately do not — an ornamental dot has no
6
+ * arc for the knob to move (R-PILL), and an avatar's circle is the object's
7
+ * identity rather than a style (R-IDENTITY). Those parts must SAY SO on the
8
+ * node, because the alternative — a probe that skips anything that looks
9
+ * round — silently exempts real KNOB-TOTALITY defects (LC-18).
10
+ *
11
+ * The declaration is only half the contract: the `radius:none` sweep
12
+ * validates it against the registry GENERATED from the spec's radius-class
13
+ * table (`scripts/radius-identity-registry.mjs`), so a class this file cannot
14
+ * spell, or an R-IDENTITY part the spec never adjudicated, still fails. This
15
+ * module is the writing end; `docs/theme-propagation-spec.md` is the registry.
16
+ */
17
+ /**
18
+ * The radius classes whose parts stay round at `borderRadius:none`.
19
+ *
20
+ * - `R-PILL` — a pill/circle at every radius value (ornamental dots, badge
21
+ * indicators, icon wells). Size still tracks the size knob.
22
+ * - `R-IDENTITY` — the shape IS the part's identity (Avatar, brand marks).
23
+ * Membership is enumerated in the spec table and enforced per part.
24
+ */
25
+ export type KnobImmuneRadiusClass = "R-PILL" | "R-IDENTITY";
26
+ export interface RadiusClassDeclaration {
27
+ "data-radius-class": KnobImmuneRadiusClass;
28
+ "data-radius-part": string;
29
+ }
30
+ /**
31
+ * Declare a knob-immune shape so the `radius:none` sweep can license it.
32
+ *
33
+ * Spread onto the outermost node of the shape — a declaration covers its own
34
+ * anatomy, so inner painted parts (an avatar's image/fallback) inherit it.
35
+ *
36
+ * @param radiusClass the class the spec's Radius table assigns the part
37
+ * @param part the part name as the spec writes it (the R-IDENTITY registry
38
+ * is matched by this name)
39
+ *
40
+ * @example
41
+ * <Frame {...radiusClassProps("R-IDENTITY", "Avatar")} circular />
42
+ */
43
+ export declare function radiusClassProps(radiusClass: KnobImmuneRadiusClass, part: string): RadiusClassDeclaration;
44
+ //# sourceMappingURL=radiusClass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radiusClass.d.ts","sourceRoot":"","sources":["../../src/theme/radiusClass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,YAAY,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC,mBAAmB,EAAE,qBAAqB,CAAC;IAC3C,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,qBAAqB,EAClC,IAAI,EAAE,MAAM,GACX,sBAAsB,CAExB"}
@@ -15,20 +15,15 @@ export declare function useReadableTextOn(fill_: string | undefined): string | u
15
15
  * tint sub-themes, where neutral text is the house default anyway).
16
16
  */
17
17
  export declare function useAccentOnSurface(): string;
18
- /**
19
- * Accent-TINTED surface step — the accent-ramp analogue of a neutral surface
20
- * tier, for selected/current chips that carry the accent language at surface
21
- * strength rather than as a solid fill (LC-05: selected state is accent; the
22
- * ViewSwitcher rider pins segmented-control strength at "tinted", not solid).
23
- *
24
- * Picks the accent step whose relative luminance sits closest to the neutral
25
- * tier it replaces, so the chip stays a surface in BOTH schemes without a
26
- * per-component light/dark fork: against this repo's stock accent ramp that
27
- * resolves to a pale violet in light and a deep violet in dark, because the
28
- * accent ramp flips with the scheme exactly as the neutral ramp does.
29
- *
30
- * Returns `undefined` when the accent ramp is absent or unparseable — callers
31
- * keep their neutral default rather than render an unthemed chip.
32
- */
33
- export declare function useAccentTintedSurface(neutralToken?: string): string | undefined;
18
+ export interface ResolveAccentTintedSurfaceInput {
19
+ /** Accent ramp steps 1..12, as raw color values; holes allowed. */
20
+ steps: (string | undefined)[];
21
+ /** Scheme paper anchor (`$color1`). */
22
+ paper?: string;
23
+ /** Scheme ink anchor (`$color12`). */
24
+ ink?: string;
25
+ }
26
+ /** Pure resolver behind {@link useAccentTintedSurface} — see its docs. */
27
+ export declare function resolveAccentTintedSurface({ steps, paper, ink, }: ResolveAccentTintedSurfaceInput): string | undefined;
28
+ export declare function useAccentTintedSurface(): string | undefined;
34
29
  //# sourceMappingURL=readableColor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"readableColor.d.ts","sourceRoot":"","sources":["../../src/theme/readableColor.ts"],"names":[],"mappings":"AA4CA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAU/E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CA8B3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,SAAY,GAAG,MAAM,GAAG,SAAS,CAuBnF"}
1
+ {"version":3,"file":"readableColor.d.ts","sourceRoot":"","sources":["../../src/theme/readableColor.ts"],"names":[],"mappings":"AA4CA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAU/E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CA8B3C;AA0BD,MAAM,WAAW,+BAA+B;IAC9C,mEAAmE;IACnE,KAAK,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC9B,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,0EAA0E;AAC1E,wBAAgB,0BAA0B,CAAC,EACzC,KAAK,EACL,KAAK,EACL,GAAG,GACJ,EAAE,+BAA+B,GAAG,MAAM,GAAG,SAAS,CAQtD;AAED,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,SAAS,CAW3D"}