inkhouse 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -0
- package/bin/inkhouse.mjs +171 -0
- package/code.js +11802 -0
- package/manifest.json +30 -0
- package/package.json +45 -0
- package/scanner/blob-placement-regression.ts +132 -0
- package/scanner/class-collector.ts +69 -0
- package/scanner/cli.ts +336 -0
- package/scanner/component-scanner.ts +2876 -0
- package/scanner/css-patch-regression.ts +112 -0
- package/scanner/css-token-reader-regression.ts +92 -0
- package/scanner/css-token-reader.ts +477 -0
- package/scanner/font-style-resolver-regression.ts +32 -0
- package/scanner/index.ts +9 -0
- package/scanner/radial-gradient-regression.ts +53 -0
- package/scanner/style-map.ts +145 -0
- package/scanner/tailwind-parser.ts +644 -0
- package/scanner/transform-math-regression.ts +42 -0
- package/scanner/types.ts +298 -0
- package/src/blob-placement.ts +111 -0
- package/src/change-detection.ts +204 -0
- package/src/class-utils.ts +105 -0
- package/src/clip-path-decorative.ts +194 -0
- package/src/color-resolver.ts +98 -0
- package/src/colors.ts +196 -0
- package/src/component-defs.ts +54 -0
- package/src/component-gen.ts +561 -0
- package/src/component-lookup.ts +82 -0
- package/src/config.ts +115 -0
- package/src/design-system.ts +59 -0
- package/src/dev-server.ts +173 -0
- package/src/figma-globals.d.ts +3 -0
- package/src/font-style-resolver.ts +171 -0
- package/src/github.ts +1465 -0
- package/src/icon-builder.ts +607 -0
- package/src/image-cache.ts +22 -0
- package/src/inline-text.ts +271 -0
- package/src/layout-parser.ts +667 -0
- package/src/layout-utils.ts +155 -0
- package/src/main.ts +687 -0
- package/src/node-ir.ts +595 -0
- package/src/pack-provider.ts +148 -0
- package/src/packs.ts +126 -0
- package/src/radial-gradient.ts +84 -0
- package/src/render-context.ts +138 -0
- package/src/responsive-analyzer.ts +139 -0
- package/src/state-analyzer.ts +143 -0
- package/src/story-builder.ts +1706 -0
- package/src/story-layout.ts +38 -0
- package/src/tailwind.ts +2379 -0
- package/src/text-builder.ts +116 -0
- package/src/text-line.ts +42 -0
- package/src/token-source.ts +43 -0
- package/src/tokens.ts +717 -0
- package/src/transform-math.ts +44 -0
- package/src/ui-builder.ts +1996 -0
- package/src/utility-resolver.ts +125 -0
- package/src/variables.ts +1042 -0
- package/src/width-solver.ts +466 -0
- package/templates/patch-tokens-route.ts +165 -0
- package/templates/scan-components-route.ts +57 -0
- package/ui.html +1222 -0
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { parseColor, mergeTokens as deepMergeTokens } from './colors';
|
|
2
|
+
import { readVariableTokens } from './variables';
|
|
3
|
+
import type { ScannedTokenMap } from './token-source';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Types
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/** A flat record of token name -> value (e.g. color name -> oklch string). */
|
|
10
|
+
export type TokenGroup = Record<string, string>;
|
|
11
|
+
|
|
12
|
+
/** A theme block contains optional color, radius, spacing, fontSize, and shadow groups. */
|
|
13
|
+
export interface ThemeTokens {
|
|
14
|
+
color?: TokenGroup;
|
|
15
|
+
font?: TokenGroup;
|
|
16
|
+
radius?: TokenGroup;
|
|
17
|
+
spacing?: TokenGroup;
|
|
18
|
+
fontSize?: TokenGroup;
|
|
19
|
+
shadow?: TokenGroup;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Core (non-theme) tokens. */
|
|
23
|
+
export interface CoreTokens {
|
|
24
|
+
font?: TokenGroup;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Top-level tokens structure with core + named themes. */
|
|
28
|
+
export interface Tokens {
|
|
29
|
+
core?: CoreTokens;
|
|
30
|
+
primary?: ThemeTokens;
|
|
31
|
+
secondary?: ThemeTokens;
|
|
32
|
+
tertiary?: ThemeTokens;
|
|
33
|
+
[theme: string]: ThemeTokens | CoreTokens | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** DTCG token leaf node. */
|
|
37
|
+
export interface DTCGEntry {
|
|
38
|
+
$type: string;
|
|
39
|
+
$value: string;
|
|
40
|
+
$description?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** DTCG output structure (recursive groups with $metadata at root). */
|
|
44
|
+
export interface DTCGTokens {
|
|
45
|
+
$metadata?: {
|
|
46
|
+
description: string;
|
|
47
|
+
note: string;
|
|
48
|
+
};
|
|
49
|
+
[key: string]: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface VariableTokenDiffOptions {
|
|
53
|
+
allowNewTokensFromFigma?: boolean;
|
|
54
|
+
newTokenPrefixes?: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Component definitions fetched from dev server. */
|
|
58
|
+
export interface ComponentDefs {
|
|
59
|
+
schemaVersion?: number;
|
|
60
|
+
version?: string;
|
|
61
|
+
generatedAt?: string;
|
|
62
|
+
components: any[];
|
|
63
|
+
spacingScale: Record<string, any>;
|
|
64
|
+
colorTokens: any[];
|
|
65
|
+
paletteTokens?: Record<string, string>;
|
|
66
|
+
iconRegistry?: Record<string, { module: string; exportName: string; svg: string }>;
|
|
67
|
+
styleMap?: Record<string, { declarations: Record<string, string>; media?: string }[]>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Mutable state
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
export let TOKENS: Tokens = {"primary":{"color":{"background":"oklch(1 0 0)","foreground":"oklch(0.141 0.005 285.823)","card":"oklch(1 0 0)","card-foreground":"oklch(0.141 0.005 285.823)","popover":"oklch(1 0 0)","popover-foreground":"oklch(0.141 0.005 285.823)","primary":"oklch(62.71% 0.17 149.21)","primary-foreground":"oklch(0.982 0.018 155.826)","secondary":"oklch(0.967 0.001 286.375)","secondary-foreground":"oklch(0.21 0.006 285.885)","muted":"oklch(0.967 0.001 286.375)","muted-foreground":"oklch(0.552 0.016 285.938)","accent":"oklch(0.967 0.001 286.375)","accent-foreground":"oklch(0.21 0.006 285.885)","destructive":"oklch(0.577 0.245 27.325)","border":"oklch(0.92 0.004 286.32)","input":"oklch(0.92 0.004 286.32)","ring":"oklch(0.723 0.219 149.579)","sidebar":"oklch(0.985 0 0)","sidebar-foreground":"oklch(0.141 0.005 285.823)","sidebar-primary":"oklch(0.723 0.219 149.579)","sidebar-primary-foreground":"oklch(0.982 0.018 155.826)","sidebar-accent":"oklch(0.967 0.001 286.375)","sidebar-accent-foreground":"oklch(0.21 0.006 285.885)","sidebar-border":"oklch(0.92 0.004 286.32)","sidebar-ring":"oklch(0.723 0.219 149.579)"},"font":{"sans":"\"Open Sans\", ui-sans-serif, system-ui, sans-serif","heading":"\"Open Sans\", ui-sans-serif, system-ui, sans-serif"},"radius":{"base":"0.5rem","sm":"0.25rem","md":"0.5rem","lg":"0.75rem","xl":"1rem","2xl":"1.25rem","full":"624rem"},"spacing":{"xs":"0.25rem","sm":"0.5rem","md":"1rem","lg":"1.5rem","xl":"2rem","2xl":"3rem","3xl":"4rem","4xl":"6rem"},"fontSize":{"xs":"0.75rem","sm":"0.875rem","base":"1rem","lg":"1.125rem","xl":"1.25rem","2xl":"1.5rem","3xl":"1.875rem","4xl":"2.25rem","5xl":"3rem","6xl":"3.75rem"},"shadow":{"sm":"0 1px 2px 0 rgb(0 0 0 / 0.05)","DEFAULT":"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)","md":"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)","lg":"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.05)","xl":"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.04)","2xl":"0 25px 50px -12px rgb(0 0 0 / 0.25)","inner":"inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"}},"secondary":{"color":{"background":"oklch(1 0 0)","foreground":"oklch(0.141 0.005 285.823)","card":"oklch(1 0 0)","card-foreground":"oklch(0.141 0.005 285.823)","popover":"oklch(1 0 0)","popover-foreground":"oklch(0.141 0.005 285.823)","primary":"oklch(62.71% 0.17 250)","primary-foreground":"oklch(0.982 0.018 250)","secondary":"oklch(0.967 0.001 286.375)","secondary-foreground":"oklch(0.21 0.006 285.885)","muted":"oklch(0.967 0.001 286.375)","muted-foreground":"oklch(0.552 0.016 285.938)","accent":"oklch(0.967 0.001 286.375)","accent-foreground":"oklch(0.21 0.006 285.885)","destructive":"oklch(0.577 0.245 250)","border":"oklch(0.92 0.004 286.32)","input":"oklch(0.92 0.004 286.32)","ring":"oklch(0.723 0.219 250)","sidebar":"oklch(0.985 0 0)","sidebar-foreground":"oklch(0.141 0.005 285.823)","sidebar-primary":"oklch(0.723 0.219 250)","sidebar-primary-foreground":"oklch(0.982 0.018 250)","sidebar-accent":"oklch(0.967 0.001 286.375)","sidebar-accent-foreground":"oklch(0.21 0.006 285.885)","sidebar-border":"oklch(0.92 0.004 286.32)","sidebar-ring":"oklch(0.723 0.219 250)"},"font":{"sans":"\"Inter\", ui-sans-serif, system-ui, sans-serif","heading":"\"Playfair Display\", Georgia, serif"},"radius":{"base":"1rem","sm":"0.75rem","md":"1rem","lg":"1.25rem","xl":"1.5rem","2xl":"1.75rem","full":"624rem"}}};
|
|
75
|
+
const EMBEDDED_TOKENS_SNAPSHOT: Tokens = JSON.parse(JSON.stringify(TOKENS));
|
|
76
|
+
|
|
77
|
+
// Component definitions - fetched from dev server at runtime
|
|
78
|
+
// This starts empty and is populated by fetchComponentDefsFromServer()
|
|
79
|
+
export let COMPONENT_DEFS: ComponentDefs = { components: [], spacingScale: {}, colorTokens: [], paletteTokens: {}, iconRegistry: {} };
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Internal helpers
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
86
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mergeTokens(base: any, override: any): any {
|
|
90
|
+
if (!isPlainObject(base)) return override;
|
|
91
|
+
const out: Record<string, any> = {};
|
|
92
|
+
for (const key in base) out[key] = base[key];
|
|
93
|
+
for (const key in override) {
|
|
94
|
+
if (isPlainObject(base[key]) && isPlainObject(override[key])) {
|
|
95
|
+
out[key] = mergeTokens(base[key], override[key]);
|
|
96
|
+
} else {
|
|
97
|
+
out[key] = override[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseDimensionToPx(value: unknown): number | null {
|
|
104
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
105
|
+
const raw = String(value || '').trim();
|
|
106
|
+
if (!raw) return null;
|
|
107
|
+
if (raw.endsWith('px')) {
|
|
108
|
+
const n = parseFloat(raw.slice(0, -2));
|
|
109
|
+
return Number.isFinite(n) ? n : null;
|
|
110
|
+
}
|
|
111
|
+
if (raw.endsWith('rem')) {
|
|
112
|
+
const n = parseFloat(raw.slice(0, -3));
|
|
113
|
+
return Number.isFinite(n) ? n * 16 : null;
|
|
114
|
+
}
|
|
115
|
+
if (/^-?[0-9.]+$/.test(raw)) {
|
|
116
|
+
const n = parseFloat(raw);
|
|
117
|
+
return Number.isFinite(n) ? n : null;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeFontFamily(value: unknown): string {
|
|
123
|
+
return String(value || '')
|
|
124
|
+
.split(',')[0]
|
|
125
|
+
.trim()
|
|
126
|
+
.replace(/^["']|["']$/g, '')
|
|
127
|
+
.toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function valuesAreSemanticallyEqual(path: string[], baseValue: unknown, nextValue: unknown): boolean {
|
|
131
|
+
if (baseValue === undefined || nextValue === undefined) return false;
|
|
132
|
+
const group = path.length >= 2 ? path[1] : '';
|
|
133
|
+
|
|
134
|
+
if (group === 'color') {
|
|
135
|
+
try {
|
|
136
|
+
const a = parseColor(baseValue);
|
|
137
|
+
const b = parseColor(nextValue);
|
|
138
|
+
const eps = 1 / 255 + 1e-6;
|
|
139
|
+
return (
|
|
140
|
+
Math.abs(a.r - b.r) <= eps &&
|
|
141
|
+
Math.abs(a.g - b.g) <= eps &&
|
|
142
|
+
Math.abs(a.b - b.b) <= eps &&
|
|
143
|
+
Math.abs((a.a == null ? 1 : a.a) - (b.a == null ? 1 : b.a)) <= 0.01
|
|
144
|
+
);
|
|
145
|
+
} catch (_e) {
|
|
146
|
+
return String(baseValue).trim() === String(nextValue).trim();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (group === 'radius' || group === 'spacing' || group === 'fontSize') {
|
|
151
|
+
const a = parseDimensionToPx(baseValue);
|
|
152
|
+
const b = parseDimensionToPx(nextValue);
|
|
153
|
+
if (a != null && b != null) return Math.abs(a - b) < 0.001;
|
|
154
|
+
return String(baseValue).trim() === String(nextValue).trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (group === 'font') {
|
|
158
|
+
return normalizeFontFamily(baseValue) === normalizeFontFamily(nextValue);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return String(baseValue).trim() === String(nextValue).trim();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasLeafValues(node: unknown): boolean {
|
|
165
|
+
if (!isPlainObject(node)) return node !== undefined;
|
|
166
|
+
for (const key in node) {
|
|
167
|
+
if (hasLeafValues((node as Record<string, unknown>)[key])) return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isColorValueObject(value: unknown): boolean {
|
|
173
|
+
if (!isPlainObject(value)) return false;
|
|
174
|
+
const candidate = value as Record<string, unknown>;
|
|
175
|
+
return (
|
|
176
|
+
typeof candidate.r === 'number' &&
|
|
177
|
+
typeof candidate.g === 'number' &&
|
|
178
|
+
typeof candidate.b === 'number' &&
|
|
179
|
+
(candidate.a === undefined || typeof candidate.a === 'number')
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isKnownThemeGroupName(name: string): boolean {
|
|
184
|
+
return (
|
|
185
|
+
name === 'color' ||
|
|
186
|
+
name === 'radius' ||
|
|
187
|
+
name === 'font' ||
|
|
188
|
+
name === 'spacing' ||
|
|
189
|
+
name === 'fontSize' ||
|
|
190
|
+
name === 'shadow'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildVariableDiffPatch(
|
|
195
|
+
base: unknown,
|
|
196
|
+
next: unknown,
|
|
197
|
+
path: string[] = [],
|
|
198
|
+
options?: VariableTokenDiffOptions
|
|
199
|
+
): Record<string, unknown> | unknown | undefined {
|
|
200
|
+
const treatNextAsLeaf = !isPlainObject(next) || isColorValueObject(next);
|
|
201
|
+
if (treatNextAsLeaf) {
|
|
202
|
+
if (valuesAreSemanticallyEqual(path, base, next)) return undefined;
|
|
203
|
+
return next;
|
|
204
|
+
}
|
|
205
|
+
if (!isPlainObject(base) || isColorValueObject(base)) {
|
|
206
|
+
// Only allow new leaf tokens under known theme groups when explicitly enabled.
|
|
207
|
+
const allowNew = options && options.allowNewTokensFromFigma === true;
|
|
208
|
+
if (!allowNew) return undefined;
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const allowNew = options && options.allowNewTokensFromFigma === true;
|
|
213
|
+
const allowedPrefixes = (options && options.newTokenPrefixes) ? options.newTokenPrefixes : [];
|
|
214
|
+
|
|
215
|
+
const out: Record<string, unknown> = {};
|
|
216
|
+
for (const key in next) {
|
|
217
|
+
const hasBaseKey = key in (base as Record<string, unknown>);
|
|
218
|
+
if (!hasBaseKey) {
|
|
219
|
+
const nextValue = (next as Record<string, unknown>)[key];
|
|
220
|
+
// Always allow brand-new top-level themes from Figma (e.g. "client2"),
|
|
221
|
+
// so theme creation does not silently drop to sparse diffs.
|
|
222
|
+
if (path.length === 0 && key !== 'core' && isPlainObject(nextValue)) {
|
|
223
|
+
out[key] = nextValue;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!allowNew) continue;
|
|
228
|
+
const parentTheme = path[0] || '';
|
|
229
|
+
const parentGroup = path[1] || '';
|
|
230
|
+
// Allow adding a missing known token group under an existing theme.
|
|
231
|
+
if (path.length === 1 && parentTheme !== 'core' && isKnownThemeGroupName(key) && isPlainObject(nextValue)) {
|
|
232
|
+
out[key] = nextValue;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const isKnownGroup =
|
|
236
|
+
parentGroup === 'color' ||
|
|
237
|
+
parentGroup === 'radius' ||
|
|
238
|
+
parentGroup === 'font' ||
|
|
239
|
+
parentGroup === 'spacing' ||
|
|
240
|
+
parentGroup === 'fontSize' ||
|
|
241
|
+
parentGroup === 'shadow';
|
|
242
|
+
const isLeaf = !isPlainObject(nextValue);
|
|
243
|
+
const isThemeScope = path.length >= 2 && parentTheme !== 'core';
|
|
244
|
+
if (!isLeaf || !isKnownGroup || !isThemeScope) continue;
|
|
245
|
+
|
|
246
|
+
if (allowedPrefixes.length > 0) {
|
|
247
|
+
let prefixMatch = false;
|
|
248
|
+
for (let i = 0; i < allowedPrefixes.length; i++) {
|
|
249
|
+
if (key.startsWith(allowedPrefixes[i])) {
|
|
250
|
+
prefixMatch = true;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!prefixMatch) continue;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const child = buildVariableDiffPatch(
|
|
259
|
+
(base as Record<string, unknown>)[key],
|
|
260
|
+
(next as Record<string, unknown>)[key],
|
|
261
|
+
path.concat(key),
|
|
262
|
+
options
|
|
263
|
+
);
|
|
264
|
+
if (child !== undefined && hasLeafValues(child)) out[key] = child;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return Object.keys(out).length ? out : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function numberToDimensionToken(value: number): string {
|
|
271
|
+
if (!Number.isFinite(value)) return '0';
|
|
272
|
+
const rounded = Math.round(value * 1000) / 1000;
|
|
273
|
+
const rem = rounded / 16;
|
|
274
|
+
if (Math.abs(rem * 16 - rounded) < 0.001) {
|
|
275
|
+
const remStr = String(Math.round(rem * 1000) / 1000).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
|
|
276
|
+
return `${remStr}rem`;
|
|
277
|
+
}
|
|
278
|
+
const pxStr = String(rounded).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
|
|
279
|
+
return `${pxStr}px`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function mapDimensionGroup(group: Record<string, number>): TokenGroup {
|
|
283
|
+
const out: TokenGroup = {};
|
|
284
|
+
for (const key in group) {
|
|
285
|
+
const value = group[key];
|
|
286
|
+
if (!Number.isFinite(value)) continue;
|
|
287
|
+
out[key] = numberToDimensionToken(value);
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function hasKeys(group: Record<string, unknown> | undefined): boolean {
|
|
293
|
+
return !!group && Object.keys(group).length > 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildTokenPatchFromScanned(map: ScannedTokenMap): Tokens {
|
|
297
|
+
const patch: Tokens = {};
|
|
298
|
+
|
|
299
|
+
const primary: ThemeTokens = {};
|
|
300
|
+
if (hasKeys(map.colors)) primary.color = { ...map.colors };
|
|
301
|
+
if (hasKeys(map.fonts)) primary.font = { ...map.fonts };
|
|
302
|
+
if (hasKeys(map.radius)) primary.radius = mapDimensionGroup(map.radius);
|
|
303
|
+
if (hasKeys(map.spacing)) primary.spacing = mapDimensionGroup(map.spacing);
|
|
304
|
+
if (hasKeys(map.fontSize)) primary.fontSize = mapDimensionGroup(map.fontSize);
|
|
305
|
+
if (hasKeys(map.shadows)) primary.shadow = { ...map.shadows };
|
|
306
|
+
if (Object.keys(primary).length > 0) patch.primary = primary;
|
|
307
|
+
|
|
308
|
+
for (const themeName in map.themes) {
|
|
309
|
+
const scanned = map.themes[themeName];
|
|
310
|
+
if (!scanned) continue;
|
|
311
|
+
const themedPatch: ThemeTokens = {};
|
|
312
|
+
if (hasKeys(scanned.colors)) themedPatch.color = { ...scanned.colors };
|
|
313
|
+
if (hasKeys(scanned.fonts)) themedPatch.font = { ...scanned.fonts };
|
|
314
|
+
if (hasKeys(scanned.radius)) themedPatch.radius = mapDimensionGroup(scanned.radius as Record<string, number>);
|
|
315
|
+
if (hasKeys(scanned.spacing)) themedPatch.spacing = mapDimensionGroup(scanned.spacing as Record<string, number>);
|
|
316
|
+
if (hasKeys(scanned.fontSize)) themedPatch.fontSize = mapDimensionGroup(scanned.fontSize as Record<string, number>);
|
|
317
|
+
if (hasKeys(scanned.shadows)) themedPatch.shadow = { ...(scanned.shadows as Record<string, string>) };
|
|
318
|
+
if (Object.keys(themedPatch).length > 0) patch[themeName] = themedPatch;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return patch;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function cloneTokens<T>(value: T): T {
|
|
325
|
+
return JSON.parse(JSON.stringify(value));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildTokensFromScanned(map: ScannedTokenMap, baseSnapshot: Tokens): Tokens {
|
|
329
|
+
const scannedPatch = buildTokenPatchFromScanned(map);
|
|
330
|
+
const out: Tokens = {};
|
|
331
|
+
|
|
332
|
+
// Keep core as-is (scanner currently maps fonts into theme blocks).
|
|
333
|
+
if (baseSnapshot.core) {
|
|
334
|
+
out.core = cloneTokens(baseSnapshot.core);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const basePrimary = (baseSnapshot.primary && typeof baseSnapshot.primary === 'object')
|
|
338
|
+
? (baseSnapshot.primary as ThemeTokens)
|
|
339
|
+
: {};
|
|
340
|
+
const scannedPrimary = (scannedPatch.primary && typeof scannedPatch.primary === 'object')
|
|
341
|
+
? (scannedPatch.primary as ThemeTokens)
|
|
342
|
+
: {};
|
|
343
|
+
const primaryTheme = mergeTokens(cloneTokens(basePrimary), scannedPrimary) as ThemeTokens;
|
|
344
|
+
out.primary = primaryTheme;
|
|
345
|
+
|
|
346
|
+
const scannedThemeNames = Object.keys(map.themes || {}).filter((name) => name && name !== 'primary');
|
|
347
|
+
for (let i = 0; i < scannedThemeNames.length; i++) {
|
|
348
|
+
const themeName = scannedThemeNames[i];
|
|
349
|
+
const scannedTheme = scannedPatch[themeName];
|
|
350
|
+
if (!scannedTheme || typeof scannedTheme !== 'object') continue;
|
|
351
|
+
// CSS theme blocks are often overrides; inherit from primary and apply overrides.
|
|
352
|
+
out[themeName] = mergeTokens(cloneTokens(primaryTheme), scannedTheme as ThemeTokens) as ThemeTokens;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function applyScannedTokens(map: ScannedTokenMap | null | undefined): void {
|
|
359
|
+
if (!map || map.mode === 'embedded') {
|
|
360
|
+
TOKENS = cloneTokens(EMBEDDED_TOKENS_SNAPSHOT);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const base = cloneTokens(EMBEDDED_TOKENS_SNAPSHOT) as Tokens;
|
|
365
|
+
TOKENS = buildTokensFromScanned(map, base);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function maybeApplyVariableTokens(): boolean {
|
|
369
|
+
try {
|
|
370
|
+
const varTokens = readVariableTokens();
|
|
371
|
+
if (varTokens) {
|
|
372
|
+
TOKENS = mergeTokens(TOKENS, varTokens);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
} catch (_err) {
|
|
376
|
+
// Variable reading failed - continue with embedded tokens
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Merge only meaningful variable edits into TOKENS.
|
|
383
|
+
* Keeps original token formats (oklch/rem/font stacks) for unchanged values and
|
|
384
|
+
* ignores unknown keys that are not present in the current token schema.
|
|
385
|
+
*/
|
|
386
|
+
export function applyVariableTokenDiffs(): boolean {
|
|
387
|
+
return applyVariableTokenDiffsWithOptions({});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function getVariableTokenDiffWithOptions(options: VariableTokenDiffOptions): Tokens | null {
|
|
391
|
+
try {
|
|
392
|
+
const varTokens = readVariableTokens();
|
|
393
|
+
if (!varTokens) return null;
|
|
394
|
+
const diffPatch = buildVariableDiffPatch(TOKENS as unknown, varTokens as unknown, [], options);
|
|
395
|
+
if (!diffPatch || !hasLeafValues(diffPatch) || !isPlainObject(diffPatch)) return null;
|
|
396
|
+
return diffPatch as Tokens;
|
|
397
|
+
} catch (_err) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function applyTokenPatch(patch: Tokens | null | undefined): boolean {
|
|
403
|
+
if (!patch || !hasLeafValues(patch)) return false;
|
|
404
|
+
TOKENS = deepMergeTokens(TOKENS as unknown, patch as unknown) as Tokens;
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function applyVariableTokenDiffsWithOptions(options: VariableTokenDiffOptions): boolean {
|
|
409
|
+
return applyTokenPatch(getVariableTokenDiffWithOptions(options));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Theme access helpers (safe type narrowing)
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
/** Get color tokens for a named theme, or empty object if not found. */
|
|
417
|
+
export function getThemeColors(tokens: Tokens, theme: string): TokenGroup {
|
|
418
|
+
const block = tokens[theme];
|
|
419
|
+
if (block && 'color' in block) {
|
|
420
|
+
const themeBlock = block as ThemeTokens;
|
|
421
|
+
return themeBlock.color || {};
|
|
422
|
+
}
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Get radius tokens for a named theme, or empty object if not found. */
|
|
427
|
+
export function getThemeRadius(tokens: Tokens, theme: string): TokenGroup {
|
|
428
|
+
const block = tokens[theme];
|
|
429
|
+
if (block && 'radius' in block) {
|
|
430
|
+
const themeBlock = block as ThemeTokens;
|
|
431
|
+
return themeBlock.radius || {};
|
|
432
|
+
}
|
|
433
|
+
return {};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Get spacing tokens for a named theme, falling back to primary. */
|
|
437
|
+
export function getThemeSpacing(tokens: Tokens, theme: string): TokenGroup {
|
|
438
|
+
const block = tokens[theme];
|
|
439
|
+
if (block && 'spacing' in block) {
|
|
440
|
+
const themeBlock = block as ThemeTokens;
|
|
441
|
+
if (themeBlock.spacing && Object.keys(themeBlock.spacing).length) return themeBlock.spacing;
|
|
442
|
+
}
|
|
443
|
+
if (theme !== 'primary') return getThemeSpacing(tokens, 'primary');
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Get fontSize tokens for a named theme, falling back to primary. */
|
|
448
|
+
export function getThemeFontSize(tokens: Tokens, theme: string): TokenGroup {
|
|
449
|
+
const block = tokens[theme];
|
|
450
|
+
if (block && 'fontSize' in block) {
|
|
451
|
+
const themeBlock = block as ThemeTokens;
|
|
452
|
+
if (themeBlock.fontSize && Object.keys(themeBlock.fontSize).length) return themeBlock.fontSize;
|
|
453
|
+
}
|
|
454
|
+
if (theme !== 'primary') return getThemeFontSize(tokens, 'primary');
|
|
455
|
+
return {};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Get named theme keys in declaration order, excluding the core block. */
|
|
459
|
+
export function getThemeNames(tokens: Tokens): string[] {
|
|
460
|
+
return Object.keys(tokens).filter((key) => {
|
|
461
|
+
if (key === 'core') return false;
|
|
462
|
+
const block = tokens[key];
|
|
463
|
+
if (!block || typeof block !== 'object') return false;
|
|
464
|
+
return Boolean(
|
|
465
|
+
('color' in block && (block as ThemeTokens).color && Object.keys((block as ThemeTokens).color || {}).length) ||
|
|
466
|
+
('font' in block && (block as ThemeTokens).font && Object.keys((block as ThemeTokens).font || {}).length) ||
|
|
467
|
+
('radius' in block && (block as ThemeTokens).radius && Object.keys((block as ThemeTokens).radius || {}).length)
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Get a font family for a specific theme and role ('sans' | 'heading' | 'mono').
|
|
473
|
+
* Falls back: theme+role → theme+sans → primary+role → primary+sans → null. */
|
|
474
|
+
export function getThemeFontFamily(tokens: Tokens, theme: string, role: string = 'sans'): string | null {
|
|
475
|
+
const block = tokens[theme];
|
|
476
|
+
if (block && 'font' in block) {
|
|
477
|
+
const font = (block as ThemeTokens).font;
|
|
478
|
+
if (font) {
|
|
479
|
+
const raw = (font as TokenGroup)[role] || (role !== 'sans' ? (font as TokenGroup).sans : null) || Object.values(font as TokenGroup)[0];
|
|
480
|
+
if (raw) {
|
|
481
|
+
const first = String(raw).split(',')[0].trim().replace(/^["']|["']$/g, '');
|
|
482
|
+
if (first) return first;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (theme !== 'primary') return getThemeFontFamily(tokens, 'primary', role);
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Get the primary sans-serif font family. Checks core.font first, then primary.font. */
|
|
491
|
+
export function getCoreFontFamily(tokens: Tokens): string | null {
|
|
492
|
+
const core = tokens.core;
|
|
493
|
+
if (core && core.font) {
|
|
494
|
+
const raw = (core.font as TokenGroup).sans || Object.values(core.font as TokenGroup)[0];
|
|
495
|
+
if (raw) {
|
|
496
|
+
const first = String(raw).split(',')[0].trim().replace(/^["']|["']$/g, '');
|
|
497
|
+
if (first) return first;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return getThemeFontFamily(tokens, 'primary');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Exported functions
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
/** Extract current tokens from Figma Variables for export. */
|
|
508
|
+
export function extractTokensForExport(): DTCGTokens {
|
|
509
|
+
applyVariableTokenDiffs();
|
|
510
|
+
return tokensToDTCG(TOKENS);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Convert flat TOKENS object to DTCG format (tokens.dtcg.json structure).
|
|
515
|
+
* DTCG uses { "$type": "color", "$value": "..." } for each token leaf.
|
|
516
|
+
*/
|
|
517
|
+
export function tokensToDTCG(tokens: Tokens): DTCGTokens {
|
|
518
|
+
// Static descriptions for known tokens
|
|
519
|
+
const descriptions: Record<string, string> = {
|
|
520
|
+
'core.font.sans': 'Custom sans-serif font override for Inkhouse brand',
|
|
521
|
+
'primary.radius.base': 'Base radius - all other radii are derived from this',
|
|
522
|
+
'primary.color.primary': 'Primary brand color (green)',
|
|
523
|
+
'secondary.color.primary': 'Secondary brand color (blue)',
|
|
524
|
+
'tertiary.color.primary': 'Tertiary brand color (orange)',
|
|
525
|
+
'tertiary.font.sans': 'Tertiary theme sans-serif font override'
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const dtcg: DTCGTokens = {
|
|
529
|
+
"$metadata": {
|
|
530
|
+
"description": "Inkhouse custom design tokens. Only includes tokens that override or extend Tailwind defaults.",
|
|
531
|
+
"note": "Standard Tailwind spacing, font-weights, tracking, and type scale are NOT included - those come from Tailwind itself."
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Core (font)
|
|
536
|
+
if (tokens.core) {
|
|
537
|
+
dtcg.core = {} as Record<string, any>;
|
|
538
|
+
if (tokens.core.font) {
|
|
539
|
+
dtcg.core.font = {} as Record<string, DTCGEntry>;
|
|
540
|
+
for (const fk in tokens.core.font) {
|
|
541
|
+
const fontEntry: DTCGEntry = {
|
|
542
|
+
"$type": "fontFamily",
|
|
543
|
+
"$value": tokens.core.font[fk]
|
|
544
|
+
};
|
|
545
|
+
const fontDesc = descriptions['core.font.' + fk];
|
|
546
|
+
if (fontDesc) fontEntry["$description"] = fontDesc;
|
|
547
|
+
dtcg.core.font[fk] = fontEntry;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Theme groups (primary, secondary, etc.)
|
|
553
|
+
const themes = Object.keys(tokens).filter((key) => key !== 'core');
|
|
554
|
+
for (let ti = 0; ti < themes.length; ti++) {
|
|
555
|
+
const themeName = themes[ti];
|
|
556
|
+
if (!tokens[themeName]) continue;
|
|
557
|
+
dtcg[themeName] = {} as Record<string, any>;
|
|
558
|
+
|
|
559
|
+
const themeBlock = tokens[themeName] as ThemeTokens;
|
|
560
|
+
|
|
561
|
+
// Radius
|
|
562
|
+
if (themeBlock.radius) {
|
|
563
|
+
dtcg[themeName].radius = {} as Record<string, DTCGEntry>;
|
|
564
|
+
for (const rk in themeBlock.radius) {
|
|
565
|
+
const radiusEntry: DTCGEntry = {
|
|
566
|
+
"$type": "dimension",
|
|
567
|
+
"$value": themeBlock.radius[rk]
|
|
568
|
+
};
|
|
569
|
+
const radiusDesc = descriptions[themeName + '.radius.' + rk];
|
|
570
|
+
if (radiusDesc) radiusEntry["$description"] = radiusDesc;
|
|
571
|
+
dtcg[themeName].radius[rk] = radiusEntry;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Font
|
|
576
|
+
if (themeBlock.font) {
|
|
577
|
+
dtcg[themeName].font = {} as Record<string, DTCGEntry>;
|
|
578
|
+
for (const fk in themeBlock.font) {
|
|
579
|
+
const fontEntry: DTCGEntry = {
|
|
580
|
+
"$type": "fontFamily",
|
|
581
|
+
"$value": themeBlock.font[fk]
|
|
582
|
+
};
|
|
583
|
+
const fontDesc = descriptions[themeName + '.font.' + fk];
|
|
584
|
+
if (fontDesc) fontEntry["$description"] = fontDesc;
|
|
585
|
+
dtcg[themeName].font[fk] = fontEntry;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Color
|
|
590
|
+
if (themeBlock.color) {
|
|
591
|
+
dtcg[themeName].color = {} as Record<string, DTCGEntry>;
|
|
592
|
+
for (const ck in themeBlock.color) {
|
|
593
|
+
const colorEntry: DTCGEntry = {
|
|
594
|
+
"$type": "color",
|
|
595
|
+
"$value": themeBlock.color[ck]
|
|
596
|
+
};
|
|
597
|
+
const colorDesc = descriptions[themeName + '.color.' + ck];
|
|
598
|
+
if (colorDesc) colorEntry["$description"] = colorDesc;
|
|
599
|
+
dtcg[themeName].color[ck] = colorEntry;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Spacing
|
|
604
|
+
if (themeBlock.spacing) {
|
|
605
|
+
dtcg[themeName].spacing = {} as Record<string, DTCGEntry>;
|
|
606
|
+
for (const sk in themeBlock.spacing) {
|
|
607
|
+
dtcg[themeName].spacing[sk] = { "$type": "dimension", "$value": themeBlock.spacing[sk] };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// FontSize
|
|
612
|
+
if (themeBlock.fontSize) {
|
|
613
|
+
dtcg[themeName].fontSize = {} as Record<string, DTCGEntry>;
|
|
614
|
+
for (const fk in themeBlock.fontSize) {
|
|
615
|
+
dtcg[themeName].fontSize[fk] = { "$type": "dimension", "$value": themeBlock.fontSize[fk] };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Shadow
|
|
620
|
+
if (themeBlock.shadow) {
|
|
621
|
+
dtcg[themeName].shadow = {} as Record<string, DTCGEntry>;
|
|
622
|
+
for (const sk in themeBlock.shadow) {
|
|
623
|
+
dtcg[themeName].shadow[sk] = { "$type": "shadow", "$value": themeBlock.shadow[sk] };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return dtcg;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Build tokens.css content.
|
|
633
|
+
* Generates CSS custom properties for primary (:root) and additional theme
|
|
634
|
+
* overrides (:root[data-theme="<theme>"]).
|
|
635
|
+
*/
|
|
636
|
+
export function tokensToCSS(tokens: Tokens): string {
|
|
637
|
+
const lines: string[] = [];
|
|
638
|
+
lines.push('/* Auto-generated from design-tokens/tokens.dtcg.json */');
|
|
639
|
+
lines.push('/* Custom Inkhouse tokens only - standard Tailwind values come from Tailwind itself */');
|
|
640
|
+
|
|
641
|
+
const primaryTheme = (tokens.primary as ThemeTokens) || {};
|
|
642
|
+
const primaryColors = primaryTheme.color ? primaryTheme.color : {};
|
|
643
|
+
const primaryFonts = primaryTheme.font ? primaryTheme.font : {};
|
|
644
|
+
const core = tokens.core || {};
|
|
645
|
+
const primaryRadius = primaryTheme.radius ? primaryTheme.radius : {};
|
|
646
|
+
const primarySpacing = primaryTheme.spacing ? primaryTheme.spacing : {};
|
|
647
|
+
const primaryFontSize = primaryTheme.fontSize ? primaryTheme.fontSize : {};
|
|
648
|
+
const primaryShadow = primaryTheme.shadow ? primaryTheme.shadow : {};
|
|
649
|
+
const radiusBase = primaryRadius.base || '0.5rem';
|
|
650
|
+
const themeNames = Object.keys(tokens).filter((key) => key !== 'core');
|
|
651
|
+
|
|
652
|
+
lines.push('');
|
|
653
|
+
lines.push(':root {');
|
|
654
|
+
lines.push(' --radius: ' + radiusBase + ';');
|
|
655
|
+
|
|
656
|
+
// Named radius scale
|
|
657
|
+
for (const rk in primaryRadius) {
|
|
658
|
+
if (rk === 'base') continue;
|
|
659
|
+
lines.push(' --radius-' + rk + ': ' + primaryRadius[rk] + ';');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Spacing scale
|
|
663
|
+
for (const sk in primarySpacing) {
|
|
664
|
+
lines.push(' --spacing-' + sk + ': ' + primarySpacing[sk] + ';');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// FontSize scale
|
|
668
|
+
for (const fk in primaryFontSize) {
|
|
669
|
+
lines.push(' --text-' + fk + ': ' + primaryFontSize[fk] + ';');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Shadow scale
|
|
673
|
+
for (const sk in primaryShadow) {
|
|
674
|
+
const cssKey = sk === 'DEFAULT' ? 'shadow' : 'shadow-' + sk;
|
|
675
|
+
lines.push(' --' + cssKey + ': ' + primaryShadow[sk] + ';');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Custom font family
|
|
679
|
+
const fonts = core.font || {};
|
|
680
|
+
for (const fname in fonts) {
|
|
681
|
+
lines.push(' --font-' + fname + ': ' + fonts[fname] + ';');
|
|
682
|
+
}
|
|
683
|
+
for (const fname in primaryFonts) {
|
|
684
|
+
lines.push(' --font-' + fname + ': ' + primaryFonts[fname] + ';');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Primary theme colors
|
|
688
|
+
for (const cname in primaryColors) {
|
|
689
|
+
lines.push(' --' + cname + ': ' + primaryColors[cname] + ';');
|
|
690
|
+
}
|
|
691
|
+
lines.push('}');
|
|
692
|
+
|
|
693
|
+
for (let ti = 0; ti < themeNames.length; ti++) {
|
|
694
|
+
const themeName = themeNames[ti];
|
|
695
|
+
if (themeName === 'primary') continue;
|
|
696
|
+
const themeBlock = (tokens[themeName] as ThemeTokens) || {};
|
|
697
|
+
const themeColors = themeBlock.color || {};
|
|
698
|
+
const themeFonts = themeBlock.font || {};
|
|
699
|
+
const themeRadius = themeBlock.radius || {};
|
|
700
|
+
if (!Object.keys(themeColors).length && !Object.keys(themeFonts).length && !Object.keys(themeRadius).length) continue;
|
|
701
|
+
lines.push('');
|
|
702
|
+
lines.push(':root[data-theme="' + themeName + '"] {');
|
|
703
|
+
if (themeRadius.base) {
|
|
704
|
+
lines.push(' --radius: ' + themeRadius.base + ';');
|
|
705
|
+
}
|
|
706
|
+
for (const fname in themeFonts) {
|
|
707
|
+
lines.push(' --font-' + fname + ': ' + themeFonts[fname] + ';');
|
|
708
|
+
}
|
|
709
|
+
for (const sname in themeColors) {
|
|
710
|
+
lines.push(' --' + sname + ': ' + themeColors[sname] + ';');
|
|
711
|
+
}
|
|
712
|
+
lines.push('}');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
lines.push('');
|
|
716
|
+
return lines.join('\n');
|
|
717
|
+
}
|