@multiplatform.one/theme 6.0.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +201 -0
  3. package/package.json +85 -0
  4. package/src/audit/baselines/home-login.json +171 -0
  5. package/src/audit/baselines/home-login.png +0 -0
  6. package/src/audit/baselines/home-pokemon.json +355 -0
  7. package/src/audit/baselines/home-pokemon.png +0 -0
  8. package/src/audit/baselines/home.json +194 -0
  9. package/src/audit/baselines/home.png +0 -0
  10. package/src/audit/baselines/pokemon.json +251 -0
  11. package/src/audit/baselines/pokemon.png +0 -0
  12. package/src/audit/constraintAudit.ts +874 -0
  13. package/src/audit/diffReport.spec.ts +191 -0
  14. package/src/audit/diffReport.ts +344 -0
  15. package/src/audit/index.ts +10 -0
  16. package/src/font.ts +49 -0
  17. package/src/fonts/createFontLoader.ts +15 -0
  18. package/src/fonts/createFontLoader.web.ts +9 -0
  19. package/src/fonts/index.ts +2 -0
  20. package/src/fonts/types.ts +13 -0
  21. package/src/index.ts +3 -0
  22. package/src/theme/Intent.tsx +30 -0
  23. package/src/theme/Preset.tsx +93 -0
  24. package/src/theme/PresetContext.ts +27 -0
  25. package/src/theme/Tint.tsx +44 -0
  26. package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +69 -0
  27. package/src/theme/animations/css.ts +25 -0
  28. package/src/theme/animations/index.ts +1 -0
  29. package/src/theme/animations/index.web.ts +1 -0
  30. package/src/theme/animations/reactNative.ts +54 -0
  31. package/src/theme/colorRules.spec.ts +195 -0
  32. package/src/theme/colorRules.ts +166 -0
  33. package/src/theme/cookies.spec.ts +185 -0
  34. package/src/theme/cookies.ts +209 -0
  35. package/src/theme/createDefaultThemeConfig.ts +86 -0
  36. package/src/theme/createThemes.ts +286 -0
  37. package/src/theme/devtools/ColorLineVisualizer.tsx +261 -0
  38. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +1657 -0
  39. package/src/theme/devtools/buildSnapshot.spec.ts +113 -0
  40. package/src/theme/devtools/buildSnapshot.ts +103 -0
  41. package/src/theme/devtools/knobIcons.tsx +217 -0
  42. package/src/theme/devtools/vizConstants.spec.ts +58 -0
  43. package/src/theme/devtools/vizConstants.ts +28 -0
  44. package/src/theme/index.ts +21 -0
  45. package/src/theme/intent.spec.tsx +276 -0
  46. package/src/theme/intents.ts +22 -0
  47. package/src/theme/knobs.ts +147 -0
  48. package/src/theme/pokemonScreen.spec.ts +109 -0
  49. package/src/theme/preset.spec.tsx +242 -0
  50. package/src/theme/preset.types.ts +22 -0
  51. package/src/theme/presets.spec.ts +252 -0
  52. package/src/theme/presets.ts +49 -0
  53. package/src/theme/recipes.ts +69 -0
  54. package/src/theme/resolveKnobs.spec.ts +571 -0
  55. package/src/theme/resolveKnobs.ts +226 -0
  56. package/src/theme/shared.spec.ts +84 -0
  57. package/src/theme/shared.tsx +131 -0
  58. package/src/theme/theme.native.tsx +65 -0
  59. package/src/theme/theme.tsx +190 -0
  60. package/src/theme/tint.spec.tsx +179 -0
  61. package/src/theme/useColorScale.native.ts +7 -0
  62. package/src/theme/useColorScale.ts +29 -0
  63. package/src/theme/useResolvedKnobs.ts +121 -0
  64. package/src/theme/useTheme.native.ts +49 -0
  65. package/src/theme/useTheme.ts +213 -0
  66. package/tsconfig.json +11 -0
  67. package/vitest.config.mjs +9 -0
@@ -0,0 +1,191 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AuditReport, ElementAudit, AuditSummary } from "./constraintAudit";
3
+ import { diffAuditReports, formatAuditReport } from "./diffReport";
4
+
5
+ function makeReport(overrides: Partial<AuditReport> = {}): AuditReport {
6
+ const defaultSummary: AuditSummary = {
7
+ totalElements: 10,
8
+ auditedElements: 2,
9
+ totalViolations: 0,
10
+ byProperty: {
11
+ borderRadius: 0,
12
+ borderWidth: 0,
13
+ gap: 0,
14
+ padding: 0,
15
+ fontWeight: 0,
16
+ fontFamily: 0,
17
+ inlineColor: 0,
18
+ },
19
+ };
20
+ return {
21
+ url: "http://localhost",
22
+ timestamp: "2025-01-01T00:00:00Z",
23
+ preset: "default",
24
+ scheme: "light",
25
+ summary: defaultSummary,
26
+ violations: [],
27
+ elements: [],
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ function makeElement(label: string, overrides: Partial<ElementAudit> = {}): ElementAudit {
33
+ return {
34
+ label,
35
+ tag: "div",
36
+ id: "",
37
+ classes: [],
38
+ violations: [],
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ describe("diffAuditReports", () => {
44
+ it("reports no changes for identical reports", () => {
45
+ const report = makeReport();
46
+ const diff = diffAuditReports(report, report);
47
+ expect(diff.summary.elementsAdded).toBe(0);
48
+ expect(diff.summary.elementsRemoved).toBe(0);
49
+ expect(diff.summary.elementsChanged).toBe(0);
50
+ expect(diff.summary.netViolationChange).toBe(0);
51
+ });
52
+
53
+ it("detects added elements with violations", () => {
54
+ const baseline = makeReport();
55
+ const current = makeReport({
56
+ elements: [makeElement("div.new", { violations: ["borderRadius: 11px is off-scale"] })],
57
+ });
58
+ const diff = diffAuditReports(baseline, current);
59
+ expect(diff.summary.elementsAdded).toBe(1);
60
+ expect(diff.summary.newViolations).toBe(1);
61
+ });
62
+
63
+ it("detects removed elements", () => {
64
+ const baseline = makeReport({
65
+ elements: [makeElement("div.old", { violations: ["gap: 15px is off-scale"] })],
66
+ });
67
+ const current = makeReport();
68
+ const diff = diffAuditReports(baseline, current);
69
+ expect(diff.summary.elementsRemoved).toBe(1);
70
+ expect(diff.summary.fixedViolations).toBe(1);
71
+ });
72
+
73
+ it("detects property changes on elements", () => {
74
+ const baseline = makeReport({
75
+ elements: [
76
+ makeElement("div.item", {
77
+ borderRadius: { value: 5, unit: "px", knob: "small", offScale: false },
78
+ }),
79
+ ],
80
+ });
81
+ const current = makeReport({
82
+ elements: [
83
+ makeElement("div.item", {
84
+ borderRadius: { value: 11, unit: "px", knob: "off-scale", offScale: true },
85
+ violations: ["borderRadius: 11px is off-scale"],
86
+ }),
87
+ ],
88
+ });
89
+ const diff = diffAuditReports(baseline, current);
90
+ expect(diff.summary.elementsChanged).toBe(1);
91
+ expect(diff.summary.newViolations).toBe(1);
92
+ });
93
+
94
+ it("detects fixed violations", () => {
95
+ const baseline = makeReport({
96
+ elements: [
97
+ makeElement("div.item", {
98
+ borderRadius: { value: 11, unit: "px", knob: "off-scale", offScale: true },
99
+ violations: ["borderRadius: 11px is off-scale"],
100
+ }),
101
+ ],
102
+ });
103
+ const current = makeReport({
104
+ elements: [
105
+ makeElement("div.item", {
106
+ borderRadius: { value: 5, unit: "px", knob: "small", offScale: false },
107
+ }),
108
+ ],
109
+ });
110
+ const diff = diffAuditReports(baseline, current);
111
+ expect(diff.summary.fixedViolations).toBeGreaterThanOrEqual(1);
112
+ });
113
+
114
+ it("calculates net violation change", () => {
115
+ const baseline = makeReport({
116
+ elements: [makeElement("div.a", { violations: ["old violation"] })],
117
+ });
118
+ const current = makeReport({
119
+ elements: [makeElement("div.a"), makeElement("div.b", { violations: ["new1", "new2"] })],
120
+ });
121
+ const diff = diffAuditReports(baseline, current);
122
+ expect(diff.summary.newViolations).toBe(2);
123
+ });
124
+
125
+ it("records preset change in metadata", () => {
126
+ const baseline = makeReport({ preset: "alpha" });
127
+ const current = makeReport({ preset: "beta" });
128
+ const diff = diffAuditReports(baseline, current);
129
+ expect(diff.baselinePreset).toBe("alpha");
130
+ expect(diff.currentPreset).toBe("beta");
131
+ expect(diff.report).toContain("alpha");
132
+ expect(diff.report).toContain("beta");
133
+ });
134
+
135
+ it("generates a human-readable report string", () => {
136
+ const baseline = makeReport();
137
+ const current = makeReport();
138
+ const diff = diffAuditReports(baseline, current);
139
+ expect(diff.report).toContain("# Design Constraint Audit Diff");
140
+ expect(diff.report).toContain("## Summary");
141
+ });
142
+ });
143
+
144
+ describe("formatAuditReport", () => {
145
+ it("formats a clean report", () => {
146
+ const report = makeReport({
147
+ elements: [
148
+ makeElement("div.clean", {
149
+ borderRadius: { value: 9, unit: "px", knob: "medium", offScale: false },
150
+ }),
151
+ ],
152
+ });
153
+ const text = formatAuditReport(report);
154
+ expect(text).toContain("# Design Constraint Audit Report");
155
+ expect(text).toContain("No constraint violations found");
156
+ });
157
+
158
+ it("formats a report with violations", () => {
159
+ const report = makeReport({
160
+ summary: {
161
+ totalElements: 10,
162
+ auditedElements: 1,
163
+ totalViolations: 1,
164
+ byProperty: {
165
+ borderRadius: 1,
166
+ borderWidth: 0,
167
+ gap: 0,
168
+ padding: 0,
169
+ fontWeight: 0,
170
+ fontFamily: 0,
171
+ inlineColor: 0,
172
+ },
173
+ },
174
+ violations: [
175
+ makeElement("div.bad", {
176
+ violations: ["borderRadius: 11px is off-scale"],
177
+ }),
178
+ ],
179
+ });
180
+ const text = formatAuditReport(report);
181
+ expect(text).toContain("## Violations");
182
+ expect(text).toContain("borderRadius: 11px is off-scale");
183
+ });
184
+
185
+ it("includes summary statistics", () => {
186
+ const report = makeReport();
187
+ const text = formatAuditReport(report);
188
+ expect(text).toContain("Total elements on page");
189
+ expect(text).toContain("Elements with violations");
190
+ });
191
+ });
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Audit Diff Reporter
3
+ *
4
+ * Compares two AuditReport snapshots (pre/post change) and produces
5
+ * a human-readable delta in constraint terms.
6
+ */
7
+
8
+ import type {
9
+ AuditReport,
10
+ ElementAudit,
11
+ PropertyAudit,
12
+ TextPropertyAudit,
13
+ } from "./constraintAudit";
14
+
15
+ // ── Types ─────────────────────────────────────────────────────────────────────
16
+
17
+ export interface PropertyDiff {
18
+ property: string;
19
+ before: PropertyAudit | TextPropertyAudit | undefined;
20
+ after: PropertyAudit | TextPropertyAudit | undefined;
21
+ changed: boolean;
22
+ becameViolation: boolean;
23
+ fixedViolation: boolean;
24
+ }
25
+
26
+ export interface ElementDiff {
27
+ label: string;
28
+ tag: string;
29
+ status: "added" | "removed" | "changed" | "unchanged";
30
+ propertyDiffs: PropertyDiff[];
31
+ newViolations: string[];
32
+ fixedViolations: string[];
33
+ }
34
+
35
+ export interface AuditDiff {
36
+ baselineUrl: string;
37
+ currentUrl: string;
38
+ baselineTimestamp: string;
39
+ currentTimestamp: string;
40
+ baselinePreset: string;
41
+ currentPreset: string;
42
+ summary: {
43
+ elementsAdded: number;
44
+ elementsRemoved: number;
45
+ elementsChanged: number;
46
+ newViolations: number;
47
+ fixedViolations: number;
48
+ netViolationChange: number;
49
+ };
50
+ elementDiffs: ElementDiff[];
51
+ /** Human-readable report text */
52
+ report: string;
53
+ }
54
+
55
+ // ── Helpers ───────────────────────────────────────────────────────────────────
56
+
57
+ const AUDITED_PROPS = [
58
+ "borderRadius",
59
+ "borderWidth",
60
+ "rowGap",
61
+ "columnGap",
62
+ "paddingTop",
63
+ "paddingRight",
64
+ "paddingBottom",
65
+ "paddingLeft",
66
+ "fontWeight",
67
+ "fontFamily",
68
+ "inlineColor",
69
+ ] as const;
70
+
71
+ function propKey(el: ElementAudit, prop: string): PropertyAudit | TextPropertyAudit | undefined {
72
+ return (el as unknown as Record<string, unknown>)[prop] as
73
+ | PropertyAudit
74
+ | TextPropertyAudit
75
+ | undefined;
76
+ }
77
+
78
+ function formatProp(p: PropertyAudit | TextPropertyAudit | undefined): string {
79
+ if (!p) return "(none)";
80
+ const flag = p.offScale ? " ⚠ OFF-SCALE" : "";
81
+ if ("unit" in p) {
82
+ return `${p.value}${p.unit} → ${p.knob}${flag}`;
83
+ }
84
+ return `"${p.value}" → ${p.knob}${flag}`;
85
+ }
86
+
87
+ // ── Core diff function ────────────────────────────────────────────────────────
88
+
89
+ export function diffAuditReports(baseline: AuditReport, current: AuditReport): AuditDiff {
90
+ // Index baseline elements by label for fast lookup
91
+ const baselineByLabel = new Map<string, ElementAudit>();
92
+ for (const el of baseline.elements) {
93
+ baselineByLabel.set(el.label, el);
94
+ }
95
+
96
+ const currentByLabel = new Map<string, ElementAudit>();
97
+ for (const el of current.elements) {
98
+ currentByLabel.set(el.label, el);
99
+ }
100
+
101
+ const elementDiffs: ElementDiff[] = [];
102
+ let elementsAdded = 0;
103
+ let elementsRemoved = 0;
104
+ let elementsChanged = 0;
105
+ let newViolations = 0;
106
+ let fixedViolations = 0;
107
+
108
+ // Check all current elements
109
+ for (const [label, curr] of currentByLabel) {
110
+ const prev = baselineByLabel.get(label);
111
+
112
+ if (!prev) {
113
+ // New element
114
+ elementsAdded++;
115
+ const addedViolations = curr.violations;
116
+ newViolations += addedViolations.length;
117
+ if (addedViolations.length > 0) {
118
+ elementDiffs.push({
119
+ label,
120
+ tag: curr.tag,
121
+ status: "added",
122
+ propertyDiffs: [],
123
+ newViolations: addedViolations,
124
+ fixedViolations: [],
125
+ });
126
+ }
127
+ continue;
128
+ }
129
+
130
+ // Compare properties
131
+ const propertyDiffs: PropertyDiff[] = [];
132
+ let hasChanges = false;
133
+
134
+ for (const prop of AUDITED_PROPS) {
135
+ const before = propKey(prev, prop);
136
+ const after = propKey(curr, prop);
137
+
138
+ const beforeStr = JSON.stringify(before);
139
+ const afterStr = JSON.stringify(after);
140
+ if (beforeStr === afterStr) continue;
141
+
142
+ hasChanges = true;
143
+ const becameViolation = !before?.offScale && (after?.offScale ?? false);
144
+ const fixedViolation = (before?.offScale ?? false) && !after?.offScale;
145
+
146
+ if (becameViolation) newViolations++;
147
+ if (fixedViolation) fixedViolations++;
148
+
149
+ propertyDiffs.push({
150
+ property: prop,
151
+ before,
152
+ after,
153
+ changed: true,
154
+ becameViolation,
155
+ fixedViolation,
156
+ });
157
+ }
158
+
159
+ // Check for violation changes not captured by property changes
160
+ const prevViolSet = new Set(prev.violations);
161
+ const currViolSet = new Set(curr.violations);
162
+ const newViol = curr.violations.filter((v) => !prevViolSet.has(v));
163
+ const fixedViol = prev.violations.filter((v) => !currViolSet.has(v));
164
+
165
+ if (hasChanges || newViol.length > 0 || fixedViol.length > 0) {
166
+ elementsChanged++;
167
+ elementDiffs.push({
168
+ label,
169
+ tag: curr.tag,
170
+ status: "changed",
171
+ propertyDiffs,
172
+ newViolations: newViol,
173
+ fixedViolations: fixedViol,
174
+ });
175
+ }
176
+ }
177
+
178
+ // Check for removed elements
179
+ for (const [label, prev] of baselineByLabel) {
180
+ if (!currentByLabel.has(label)) {
181
+ elementsRemoved++;
182
+ if (prev.violations.length > 0) {
183
+ fixedViolations += prev.violations.length;
184
+ elementDiffs.push({
185
+ label,
186
+ tag: prev.tag,
187
+ status: "removed",
188
+ propertyDiffs: [],
189
+ newViolations: [],
190
+ fixedViolations: prev.violations,
191
+ });
192
+ }
193
+ }
194
+ }
195
+
196
+ const netViolationChange = newViolations - fixedViolations;
197
+
198
+ // ── Format human-readable report ──────────────────────────────────────────
199
+
200
+ const lines: string[] = [];
201
+ const w = (s: string) => lines.push(s);
202
+
203
+ w("# Design Constraint Audit Diff");
204
+ w("");
205
+ w(`Baseline: ${baseline.timestamp} | ${baseline.url}`);
206
+ w(`Current: ${current.timestamp} | ${current.url}`);
207
+ if (baseline.preset !== current.preset) {
208
+ w(`Preset changed: ${baseline.preset} → ${current.preset}`);
209
+ } else {
210
+ w(`Preset: ${current.preset}`);
211
+ }
212
+ w("");
213
+ w("## Summary");
214
+ w(`- Elements added: ${elementsAdded}`);
215
+ w(`- Elements removed: ${elementsRemoved}`);
216
+ w(`- Elements changed: ${elementsChanged}`);
217
+ w(`- New violations: ${newViolations}${newViolations > 0 ? " ⚠" : " ✓"}`);
218
+ w(`- Fixed violations: ${fixedViolations}${fixedViolations > 0 ? " ✓" : ""}`);
219
+ w(`- Net change: ${netViolationChange > 0 ? "+" : ""}${netViolationChange} violations`);
220
+ w("");
221
+
222
+ const changed = elementDiffs.filter((d) => d.status === "changed" && d.propertyDiffs.length > 0);
223
+ const newViolElements = elementDiffs.filter((d) => d.newViolations.length > 0);
224
+
225
+ if (changed.length > 0) {
226
+ w("## Property Changes");
227
+ w("");
228
+ for (const diff of changed) {
229
+ w(`### \`${diff.label}\``);
230
+ for (const pd of diff.propertyDiffs) {
231
+ const flag = pd.becameViolation ? " ⚠ VIOLATION" : pd.fixedViolation ? " ✓ FIXED" : "";
232
+ w(`- **${pd.property}**: ${formatProp(pd.before)} → ${formatProp(pd.after)}${flag}`);
233
+ }
234
+ w("");
235
+ }
236
+ }
237
+
238
+ if (newViolElements.length > 0) {
239
+ w("## New Violations");
240
+ w("");
241
+ for (const diff of newViolElements) {
242
+ w(`### \`${diff.label}\` (${diff.status})`);
243
+ for (const v of diff.newViolations) {
244
+ w(`- ⚠ ${v}`);
245
+ }
246
+ if (diff.fixedViolations.length > 0) {
247
+ for (const v of diff.fixedViolations) {
248
+ w(`- ✓ Fixed: ${v}`);
249
+ }
250
+ }
251
+ w("");
252
+ }
253
+ }
254
+
255
+ if (
256
+ newViolations === 0 &&
257
+ elementsChanged === 0 &&
258
+ elementsAdded === 0 &&
259
+ elementsRemoved === 0
260
+ ) {
261
+ w("No structural changes detected. Everything looks identical.");
262
+ } else if (newViolations === 0) {
263
+ w("No new constraint violations introduced.");
264
+ }
265
+
266
+ return {
267
+ baselineUrl: baseline.url,
268
+ currentUrl: current.url,
269
+ baselineTimestamp: baseline.timestamp,
270
+ currentTimestamp: current.timestamp,
271
+ baselinePreset: baseline.preset,
272
+ currentPreset: current.preset,
273
+ summary: {
274
+ elementsAdded,
275
+ elementsRemoved,
276
+ elementsChanged,
277
+ newViolations,
278
+ fixedViolations,
279
+ netViolationChange,
280
+ },
281
+ elementDiffs,
282
+ report: lines.join("\n"),
283
+ };
284
+ }
285
+
286
+ // ── Format a single AuditReport as a readable report ─────────────────────────
287
+
288
+ export function formatAuditReport(report: AuditReport): string {
289
+ const lines: string[] = [];
290
+ const w = (s: string) => lines.push(s);
291
+
292
+ w("# Design Constraint Audit Report");
293
+ w("");
294
+ w(`URL: ${report.url}`);
295
+ w(`Timestamp: ${report.timestamp}`);
296
+ w(`Preset: ${report.preset}`);
297
+ w(`Scheme: ${report.scheme}`);
298
+ w("");
299
+ w("## Summary");
300
+ w(`- Total elements on page: ${report.summary.totalElements}`);
301
+ w(`- Elements with styled props: ${report.summary.auditedElements}`);
302
+ w(`- Elements with violations: ${report.summary.totalViolations}`);
303
+ w("");
304
+ w("Violations by property:");
305
+ w(` borderRadius: ${report.summary.byProperty.borderRadius}`);
306
+ w(` borderWidth: ${report.summary.byProperty.borderWidth}`);
307
+ w(` gap: ${report.summary.byProperty.gap}`);
308
+ w(` padding: ${report.summary.byProperty.padding}`);
309
+ w(` fontWeight: ${report.summary.byProperty.fontWeight}`);
310
+ w(` fontFamily: ${report.summary.byProperty.fontFamily}`);
311
+ w(` inlineColor: ${report.summary.byProperty.inlineColor}`);
312
+ w("");
313
+
314
+ if (report.violations.length === 0) {
315
+ w("✓ No constraint violations found.");
316
+ } else {
317
+ w("## Violations");
318
+ w("");
319
+ for (const el of report.violations) {
320
+ w(`### \`${el.label}\``);
321
+ for (const v of el.violations) {
322
+ w(`- ⚠ ${v}`);
323
+ }
324
+ w("");
325
+ }
326
+ }
327
+
328
+ if (report.elements.length > 0 && report.violations.length === 0) {
329
+ w("## Audited Elements (all compliant)");
330
+ w("");
331
+ for (const el of report.elements) {
332
+ const props: string[] = [];
333
+ if (el.borderRadius) props.push(`radius=${el.borderRadius.knob}(${el.borderRadius.value}px)`);
334
+ if (el.borderWidth) props.push(`bw=${el.borderWidth.knob}(${el.borderWidth.value}px)`);
335
+ if (el.rowGap) props.push(`gap=${el.rowGap.knob}(${el.rowGap.value}px)`);
336
+ if (el.fontWeight) props.push(`fw=${el.fontWeight.knob}`);
337
+ if (el.fontFamily) props.push(`font=${el.fontFamily.knob}`);
338
+ if (el.inlineColor) props.push(`color=${el.inlineColor.value}`);
339
+ w(`- \`${el.label}\`: ${props.join(", ")}`);
340
+ }
341
+ }
342
+
343
+ return lines.join("\n");
344
+ }
@@ -0,0 +1,10 @@
1
+ export type {
2
+ AuditReport,
3
+ AuditSummary,
4
+ ElementAudit,
5
+ PropertyAudit,
6
+ TextPropertyAudit,
7
+ } from "./constraintAudit";
8
+ export { runConstraintAudit } from "./constraintAudit";
9
+ export type { AuditDiff, ElementDiff, PropertyDiff } from "./diffReport";
10
+ export { diffAuditReports, formatAuditReport } from "./diffReport";
package/src/font.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { GenericFont } from "@tamagui/web";
2
+ import { createFont } from "@tamagui/web";
3
+
4
+ const defaults = {
5
+ size: {
6
+ 1: 11,
7
+ 2: 12,
8
+ 3: 13,
9
+ 4: 14,
10
+ 5: 15,
11
+ 6: 16,
12
+ 7: 18,
13
+ 8: 21,
14
+ 9: 28,
15
+ 10: 42,
16
+ 11: 52,
17
+ 12: 62,
18
+ 13: 72,
19
+ 14: 92,
20
+ 15: 114,
21
+ 16: 124,
22
+ } as const,
23
+ letterSpacing: {
24
+ 4: 1,
25
+ 5: 3,
26
+ 6: 3,
27
+ 9: -2,
28
+ 10: -3,
29
+ 12: -4,
30
+ } as const,
31
+ weight: {
32
+ 4: "300",
33
+ } as const,
34
+ };
35
+
36
+ export function createDefaultFont<A extends GenericFont>(
37
+ font: Partial<A> & { family: A["family"] },
38
+ ): A {
39
+ const size = font.size || defaults.size;
40
+ return createFont({
41
+ lineHeight: Object.fromEntries(
42
+ Object.entries(size).map(([k, v]) => [k, typeof v === "number" ? v * 1.2 + 6 : v]),
43
+ ) as typeof size,
44
+ letterSpacing: defaults.letterSpacing,
45
+ weight: defaults.weight,
46
+ ...font,
47
+ size,
48
+ } as unknown as A);
49
+ }
@@ -0,0 +1,15 @@
1
+ import type { FontLoader, FontLoaderConfig } from "./types";
2
+
3
+ export function createFontLoader(config: FontLoaderConfig): FontLoader {
4
+ const fontMap: Record<string, number> = {};
5
+ if (config.inter) {
6
+ fontMap.Inter = require("@tamagui/font-inter/otf/Inter-Medium.otf");
7
+ fontMap.InterBold = require("@tamagui/font-inter/otf/Inter-Bold.otf");
8
+ }
9
+ for (const font of config.fonts) {
10
+ if (font.nativeModule) {
11
+ fontMap[font.family] = font.nativeModule;
12
+ }
13
+ }
14
+ return { importFonts: () => fontMap };
15
+ }
@@ -0,0 +1,9 @@
1
+ import "@tamagui/font-inter/css/400.css";
2
+ import "@tamagui/font-inter/css/700.css";
3
+ import type { FontLoader, FontLoaderConfig } from "./types";
4
+
5
+ export function createFontLoader(_config: FontLoaderConfig): FontLoader {
6
+ // On web, fonts are loaded via CSS @font-face (side-effect imports).
7
+ // Inter CSS is imported above. Custom font CSS must be imported by the consumer.
8
+ return { importFonts: () => ({}) };
9
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./createFontLoader";
2
+ export * from "./types";
@@ -0,0 +1,13 @@
1
+ export interface FontEntry {
2
+ family: string;
3
+ nativeModule?: number;
4
+ }
5
+
6
+ export interface FontLoaderConfig {
7
+ fonts: FontEntry[];
8
+ inter?: boolean;
9
+ }
10
+
11
+ export interface FontLoader {
12
+ importFonts: () => Record<string, number>;
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./theme/index";
2
+ export * from "./font";
3
+ export * from "./fonts/index";
@@ -0,0 +1,30 @@
1
+ import type { PropsWithChildren } from "react";
2
+ import { Theme } from "tamagui";
3
+ import type { ThemeName } from "@tamagui/web";
4
+ import { IntentContext } from "./useResolvedKnobs";
5
+
6
+ export interface IntentProps extends PropsWithChildren {
7
+ /** Intent name. */
8
+ name: "accent" | "error" | "warning" | "success";
9
+ /** Override the default Tamagui theme for this intent. */
10
+ theme?: ThemeName;
11
+ }
12
+
13
+ /**
14
+ * Wraps children with an intent context and corresponding Tamagui theme.
15
+ *
16
+ * Dual responsibility:
17
+ * 1. Layer 1 (color): Activates Tamagui sub-theme (intent name or explicit theme prop)
18
+ * 2. Layer 2 (structure): Provides intent name via context so useResolvedKnobs can merge overrides
19
+ *
20
+ * If both wrapper and component prop are present, component prop wins.
21
+ * Nesting intents replaces (does not merge) — innermost wins.
22
+ */
23
+ export function Intent({ name, theme, children }: IntentProps) {
24
+ const themeName = theme ?? name;
25
+ return (
26
+ <IntentContext.Provider value={name}>
27
+ <Theme name={themeName as ThemeName}>{children}</Theme>
28
+ </IntentContext.Provider>
29
+ );
30
+ }