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/node-ir.ts
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { parseColor, type RGB } from './colors';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// JSX tree types
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export interface JsxNode {
|
|
8
|
+
type: 'element' | 'text';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JsxElement extends JsxNode {
|
|
12
|
+
type: 'element';
|
|
13
|
+
tagName: string;
|
|
14
|
+
isComponent: boolean;
|
|
15
|
+
props: Record<string, string>;
|
|
16
|
+
children: JsxNode[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface JsxText extends JsxNode {
|
|
20
|
+
type: 'text';
|
|
21
|
+
content: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// NodeIR types
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export interface NodeIRText {
|
|
29
|
+
kind: 'text';
|
|
30
|
+
text: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface NodeIRFragment {
|
|
34
|
+
kind: 'fragment';
|
|
35
|
+
children: NodeIR[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NodeIRDivider {
|
|
39
|
+
kind: 'divider';
|
|
40
|
+
direction: 'HORIZONTAL' | 'VERTICAL';
|
|
41
|
+
color: RGB;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NodeIRRing {
|
|
45
|
+
kind: 'ring';
|
|
46
|
+
ringWidth: number;
|
|
47
|
+
ringColor: RGB;
|
|
48
|
+
ringOpacity: number;
|
|
49
|
+
offsetWidth: number;
|
|
50
|
+
offsetColor: RGB | null;
|
|
51
|
+
classes: string[];
|
|
52
|
+
child: NodeIR;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface NodeIRElementBase {
|
|
56
|
+
kind: 'element' | 'component';
|
|
57
|
+
tagName: string;
|
|
58
|
+
tagLower: string;
|
|
59
|
+
props: Record<string, string>;
|
|
60
|
+
classes: string[];
|
|
61
|
+
children: NodeIR[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type NodeIRElement = NodeIRElementBase & { kind: 'element' };
|
|
65
|
+
export type NodeIRComponent = NodeIRElementBase & { kind: 'component' };
|
|
66
|
+
|
|
67
|
+
export type NodeIR =
|
|
68
|
+
| NodeIRText
|
|
69
|
+
| NodeIRFragment
|
|
70
|
+
| NodeIRElement
|
|
71
|
+
| NodeIRComponent
|
|
72
|
+
| NodeIRDivider
|
|
73
|
+
| NodeIRRing;
|
|
74
|
+
|
|
75
|
+
export type NodeIRHelpers = {
|
|
76
|
+
getComponentDefByName: (name: string) => any | null;
|
|
77
|
+
normalizeComponentDef: (raw: any) => any;
|
|
78
|
+
getCompoundClasses: (def: any, tagName: string) => string[];
|
|
79
|
+
mergeClasses: (base: string[], extra: string[]) => string[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// NodeIR helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export function splitClassName(value?: string): string[] {
|
|
87
|
+
if (!value) return [];
|
|
88
|
+
return String(value)
|
|
89
|
+
.split(/\s+/)
|
|
90
|
+
.map(c => c.replace(/^!/, '').replace(/!$/, ''))
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveNodeIR(node: JsxNode): NodeIR | null {
|
|
95
|
+
if (node.type === 'text') {
|
|
96
|
+
const textNode = node as JsxText;
|
|
97
|
+
const trimmed = textNode.content.trim();
|
|
98
|
+
if (!trimmed) return null;
|
|
99
|
+
return { kind: 'text', text: trimmed };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const el = node as JsxElement;
|
|
103
|
+
const children: NodeIR[] = [];
|
|
104
|
+
for (const child of el.children || []) {
|
|
105
|
+
const resolved = resolveNodeIR(child);
|
|
106
|
+
if (resolved) children.push(resolved);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!el.tagName) {
|
|
110
|
+
if (children.length === 1) return children[0];
|
|
111
|
+
return { kind: 'fragment', children: children };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const classes = splitClassName(el.props.className);
|
|
115
|
+
const tagLower = el.tagName.toLowerCase();
|
|
116
|
+
|
|
117
|
+
if (el.isComponent) {
|
|
118
|
+
return {
|
|
119
|
+
kind: 'component',
|
|
120
|
+
tagName: el.tagName,
|
|
121
|
+
tagLower: tagLower,
|
|
122
|
+
props: el.props,
|
|
123
|
+
classes: classes,
|
|
124
|
+
children: children,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
kind: 'element',
|
|
130
|
+
tagName: el.tagName,
|
|
131
|
+
tagLower: tagLower,
|
|
132
|
+
props: el.props,
|
|
133
|
+
classes: classes,
|
|
134
|
+
children: children,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function applyNodeTransforms(
|
|
139
|
+
node: NodeIR,
|
|
140
|
+
colorGroup: Record<string, string>,
|
|
141
|
+
helpers: NodeIRHelpers
|
|
142
|
+
): NodeIR {
|
|
143
|
+
let next = flattenComponentNodes(node, null, helpers);
|
|
144
|
+
next = transformSpaceNodes(next);
|
|
145
|
+
next = transformDivideNodes(next, colorGroup);
|
|
146
|
+
next = transformRingNodes(next, colorGroup);
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isElementLikeNode(node: NodeIR): node is NodeIRElement | NodeIRComponent | NodeIRRing {
|
|
151
|
+
return node.kind === 'element' || node.kind === 'component' || node.kind === 'ring';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// NodeIR transforms
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
function flattenComponentNodes(
|
|
159
|
+
node: NodeIR,
|
|
160
|
+
parentCompoundDef: any | null,
|
|
161
|
+
helpers: NodeIRHelpers
|
|
162
|
+
): NodeIR {
|
|
163
|
+
if (node.kind === 'text' || node.kind === 'divider') {
|
|
164
|
+
return node;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (node.kind === 'fragment') {
|
|
168
|
+
const nextChildren: NodeIR[] = [];
|
|
169
|
+
for (const child of node.children) {
|
|
170
|
+
nextChildren.push(flattenComponentNodes(child, parentCompoundDef, helpers));
|
|
171
|
+
}
|
|
172
|
+
return Object.assign({}, node, { children: nextChildren });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (node.kind === 'ring') {
|
|
176
|
+
return Object.assign({}, node, { child: flattenComponentNodes(node.child, parentCompoundDef, helpers) });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (node.kind === 'element') {
|
|
180
|
+
const nextChildren: NodeIR[] = [];
|
|
181
|
+
for (const child of node.children) {
|
|
182
|
+
nextChildren.push(flattenComponentNodes(child, parentCompoundDef, helpers));
|
|
183
|
+
}
|
|
184
|
+
return Object.assign({}, node, { children: nextChildren });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const compDef = helpers.getComponentDefByName(node.tagName);
|
|
188
|
+
const normalizedDef = compDef ? helpers.normalizeComponentDef(compDef) : null;
|
|
189
|
+
let mergedClasses = node.classes;
|
|
190
|
+
let nextParentCompoundDef = parentCompoundDef;
|
|
191
|
+
let shouldFlatten = false;
|
|
192
|
+
|
|
193
|
+
if (normalizedDef) {
|
|
194
|
+
if (normalizedDef.type === 'compound') {
|
|
195
|
+
const compoundClasses = helpers.getCompoundClasses(normalizedDef, node.tagName);
|
|
196
|
+
if (compoundClasses.length > 0) {
|
|
197
|
+
mergedClasses = helpers.mergeClasses(compoundClasses, mergedClasses);
|
|
198
|
+
}
|
|
199
|
+
nextParentCompoundDef = normalizedDef;
|
|
200
|
+
shouldFlatten = true;
|
|
201
|
+
}
|
|
202
|
+
} else if (parentCompoundDef) {
|
|
203
|
+
const inherited = helpers.getCompoundClasses(parentCompoundDef, node.tagName);
|
|
204
|
+
if (inherited.length > 0) {
|
|
205
|
+
mergedClasses = helpers.mergeClasses(inherited, mergedClasses);
|
|
206
|
+
shouldFlatten = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const nextChildren: NodeIR[] = [];
|
|
211
|
+
for (const child of node.children) {
|
|
212
|
+
nextChildren.push(flattenComponentNodes(child, nextParentCompoundDef, helpers));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (shouldFlatten) {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'element',
|
|
218
|
+
tagName: node.tagName,
|
|
219
|
+
tagLower: 'div',
|
|
220
|
+
props: node.props,
|
|
221
|
+
classes: mergedClasses,
|
|
222
|
+
children: nextChildren,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return Object.assign({}, node, {
|
|
227
|
+
classes: mergedClasses,
|
|
228
|
+
children: nextChildren,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function transformSpaceNodes(node: NodeIR): NodeIR {
|
|
233
|
+
if (node.kind === 'text' || node.kind === 'divider') {
|
|
234
|
+
return node;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (node.kind === 'fragment') {
|
|
238
|
+
const nextChildren: NodeIR[] = [];
|
|
239
|
+
for (const child of node.children) {
|
|
240
|
+
nextChildren.push(transformSpaceNodes(child));
|
|
241
|
+
}
|
|
242
|
+
return Object.assign({}, node, { children: nextChildren });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (node.kind === 'ring') {
|
|
246
|
+
return Object.assign({}, node, { child: transformSpaceNodes(node.child) });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hasGap = node.classes.some(c => c.startsWith('gap-') && !c.startsWith('gap-x-') && !c.startsWith('gap-y-'));
|
|
250
|
+
const hasGapX = node.classes.some(c => c.startsWith('gap-x-'));
|
|
251
|
+
const hasGapY = node.classes.some(c => c.startsWith('gap-y-'));
|
|
252
|
+
const nextClasses: string[] = [];
|
|
253
|
+
const extraGapClasses: string[] = [];
|
|
254
|
+
|
|
255
|
+
for (const cls of node.classes) {
|
|
256
|
+
if (cls.includes(':')) {
|
|
257
|
+
nextClasses.push(cls);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (cls.startsWith('space-x-')) {
|
|
261
|
+
if (!hasGap && !hasGapX) {
|
|
262
|
+
extraGapClasses.push(cls.replace('space-x-', 'gap-x-'));
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (cls.startsWith('space-y-')) {
|
|
267
|
+
if (!hasGap && !hasGapY) {
|
|
268
|
+
extraGapClasses.push(cls.replace('space-y-', 'gap-y-'));
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (cls === 'space-x-reverse' || cls === 'space-y-reverse') {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
nextClasses.push(cls);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const nextChildren: NodeIR[] = [];
|
|
279
|
+
for (const child of node.children) {
|
|
280
|
+
nextChildren.push(transformSpaceNodes(child));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return Object.assign({}, node, {
|
|
284
|
+
classes: nextClasses.concat(extraGapClasses),
|
|
285
|
+
children: nextChildren,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function transformDivideNodes(node: NodeIR, colorGroup: Record<string, string>): NodeIR {
|
|
290
|
+
if (node.kind === 'text' || node.kind === 'divider') {
|
|
291
|
+
return node;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (node.kind === 'fragment') {
|
|
295
|
+
const nextChildren: NodeIR[] = [];
|
|
296
|
+
for (const child of node.children) {
|
|
297
|
+
nextChildren.push(transformDivideNodes(child, colorGroup));
|
|
298
|
+
}
|
|
299
|
+
return Object.assign({}, node, { children: nextChildren });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (node.kind === 'ring') {
|
|
303
|
+
return Object.assign({}, node, { child: transformDivideNodes(node.child, colorGroup) });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const nextChildren: NodeIR[] = [];
|
|
307
|
+
for (const child of node.children) {
|
|
308
|
+
nextChildren.push(transformDivideNodes(child, colorGroup));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const divideConfig = parseDivideClasses(node.classes, colorGroup);
|
|
312
|
+
if (!divideConfig || nextChildren.length <= 1) {
|
|
313
|
+
return Object.assign({}, node, { children: nextChildren });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const filteredClasses: string[] = [];
|
|
317
|
+
for (const cls of node.classes) {
|
|
318
|
+
if (cls === 'divide-y' || cls === 'divide-x' || cls.startsWith('divide-')) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
filteredClasses.push(cls);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const spacedChildren: NodeIR[] = [];
|
|
325
|
+
for (let i = 0; i < nextChildren.length; i++) {
|
|
326
|
+
spacedChildren.push(nextChildren[i]);
|
|
327
|
+
if (i < nextChildren.length - 1) {
|
|
328
|
+
spacedChildren.push({
|
|
329
|
+
kind: 'divider',
|
|
330
|
+
direction: divideConfig.direction,
|
|
331
|
+
color: divideConfig.color,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return Object.assign({}, node, {
|
|
337
|
+
classes: filteredClasses,
|
|
338
|
+
children: spacedChildren,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function transformRingNodes(node: NodeIR, colorGroup: Record<string, string>): NodeIR {
|
|
343
|
+
if (node.kind === 'text' || node.kind === 'divider') {
|
|
344
|
+
return node;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (node.kind === 'fragment') {
|
|
348
|
+
const nextChildren: NodeIR[] = [];
|
|
349
|
+
for (const child of node.children) {
|
|
350
|
+
nextChildren.push(transformRingNodes(child, colorGroup));
|
|
351
|
+
}
|
|
352
|
+
return Object.assign({}, node, { children: nextChildren });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (node.kind === 'ring') {
|
|
356
|
+
return Object.assign({}, node, { child: transformRingNodes(node.child, colorGroup) });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const nextChildren: NodeIR[] = [];
|
|
360
|
+
for (const child of node.children) {
|
|
361
|
+
nextChildren.push(transformRingNodes(child, colorGroup));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const ringConfig = parseRingClasses(node.classes, colorGroup);
|
|
365
|
+
if (!ringConfig) {
|
|
366
|
+
return Object.assign({}, node, { children: nextChildren });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const filteredClasses: string[] = [];
|
|
370
|
+
for (const cls of node.classes) {
|
|
371
|
+
if (cls.includes(':')) {
|
|
372
|
+
const tail = cls.split(':').pop() || '';
|
|
373
|
+
if (ringConfig.usedVariant && isRingUtility(tail)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
filteredClasses.push(cls);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (isRingUtility(cls)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
filteredClasses.push(cls);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const cleanedNode = Object.assign({}, node, {
|
|
386
|
+
classes: filteredClasses,
|
|
387
|
+
children: nextChildren,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (ringConfig.width <= 0) {
|
|
391
|
+
return cleanedNode;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
kind: 'ring',
|
|
396
|
+
ringWidth: ringConfig.width,
|
|
397
|
+
ringColor: ringConfig.color,
|
|
398
|
+
ringOpacity: ringConfig.opacity,
|
|
399
|
+
offsetWidth: ringConfig.offsetWidth,
|
|
400
|
+
offsetColor: ringConfig.offsetColor,
|
|
401
|
+
classes: filteredClasses,
|
|
402
|
+
child: cleanedNode,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Divide / Ring parsing helpers
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
function parseDivideClasses(
|
|
411
|
+
classes: string[],
|
|
412
|
+
colorGroup: Record<string, string>
|
|
413
|
+
): { direction: 'HORIZONTAL' | 'VERTICAL'; color: RGB } | null {
|
|
414
|
+
const hasDivideY = classes.includes('divide-y');
|
|
415
|
+
const hasDivideX = classes.includes('divide-x');
|
|
416
|
+
if (!hasDivideY && !hasDivideX) return null;
|
|
417
|
+
|
|
418
|
+
let divideColor: RGB = { r: 0.9, g: 0.9, b: 0.9 };
|
|
419
|
+
|
|
420
|
+
for (const cls of classes) {
|
|
421
|
+
if (cls.startsWith('divide-') && cls !== 'divide-y' && cls !== 'divide-x') {
|
|
422
|
+
const colorName = cls.replace('divide-', '');
|
|
423
|
+
if (colorGroup[colorName]) {
|
|
424
|
+
divideColor = parseColor(colorGroup[colorName]);
|
|
425
|
+
} else if (colorName === 'border' && colorGroup['border']) {
|
|
426
|
+
divideColor = parseColor(colorGroup['border']);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
direction: hasDivideY ? 'HORIZONTAL' : 'VERTICAL',
|
|
433
|
+
color: divideColor,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseRingClasses(
|
|
438
|
+
classes: string[],
|
|
439
|
+
colorGroup: Record<string, string>
|
|
440
|
+
): {
|
|
441
|
+
width: number;
|
|
442
|
+
color: RGB;
|
|
443
|
+
opacity: number;
|
|
444
|
+
offsetWidth: number;
|
|
445
|
+
offsetColor: RGB | null;
|
|
446
|
+
usedVariant: boolean;
|
|
447
|
+
} | null {
|
|
448
|
+
const baseClasses: string[] = [];
|
|
449
|
+
|
|
450
|
+
for (const raw of classes) {
|
|
451
|
+
if (raw.indexOf(':') !== -1) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (isRingUtility(raw)) baseClasses.push(raw);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const baseConfig = parseRingTokens(baseClasses, colorGroup);
|
|
458
|
+
if (baseConfig) {
|
|
459
|
+
baseConfig.usedVariant = false;
|
|
460
|
+
return baseConfig;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function parseRingTokens(
|
|
467
|
+
classes: string[],
|
|
468
|
+
colorGroup: Record<string, string>
|
|
469
|
+
): {
|
|
470
|
+
width: number;
|
|
471
|
+
color: RGB;
|
|
472
|
+
opacity: number;
|
|
473
|
+
offsetWidth: number;
|
|
474
|
+
offsetColor: RGB | null;
|
|
475
|
+
usedVariant: boolean;
|
|
476
|
+
} | null {
|
|
477
|
+
let width: number | null = null;
|
|
478
|
+
let color: RGB | null = null;
|
|
479
|
+
let opacity: number | null = null;
|
|
480
|
+
let offsetWidth: number | null = null;
|
|
481
|
+
let offsetColor: RGB | null = null;
|
|
482
|
+
let sawRing = false;
|
|
483
|
+
|
|
484
|
+
for (const raw of classes) {
|
|
485
|
+
if (raw === 'ring') {
|
|
486
|
+
sawRing = true;
|
|
487
|
+
width = width == null ? 3 : width;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const widthMatch = raw.match(/^ring-(\d+)$/);
|
|
491
|
+
if (widthMatch) {
|
|
492
|
+
sawRing = true;
|
|
493
|
+
width = parseInt(widthMatch[1], 10);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const bracketMatch = raw.match(/^ring-\[(\d+(?:\.\d+)?)px\]$/);
|
|
497
|
+
if (bracketMatch) {
|
|
498
|
+
sawRing = true;
|
|
499
|
+
width = parseFloat(bracketMatch[1]);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const opacityMatch = raw.match(/^ring-opacity-(\d+)$/);
|
|
503
|
+
if (opacityMatch) {
|
|
504
|
+
sawRing = true;
|
|
505
|
+
opacity = Math.max(0, Math.min(1, parseInt(opacityMatch[1], 10) / 100));
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (raw === 'ring-inset') {
|
|
509
|
+
sawRing = true;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (raw.startsWith('ring-offset-')) {
|
|
513
|
+
sawRing = true;
|
|
514
|
+
const offsetRaw = raw.slice('ring-offset-'.length);
|
|
515
|
+
const offsetBracket = offsetRaw.match(/^\[(.+)\]$/);
|
|
516
|
+
if (offsetBracket) {
|
|
517
|
+
const inner = offsetBracket[1];
|
|
518
|
+
if (isLikelyColorValue(inner)) {
|
|
519
|
+
offsetColor = parseColor(inner);
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (/^\d+(?:\.\d+)?$/.test(offsetRaw)) {
|
|
524
|
+
offsetWidth = parseFloat(offsetRaw);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (colorGroup[offsetRaw]) {
|
|
528
|
+
offsetColor = parseColor(colorGroup[offsetRaw]);
|
|
529
|
+
}
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (raw.startsWith('ring-')) {
|
|
533
|
+
sawRing = true;
|
|
534
|
+
const tokenRaw = raw.slice('ring-'.length);
|
|
535
|
+
const parts = tokenRaw.split('/');
|
|
536
|
+
const token = parts[0];
|
|
537
|
+
if (parts.length > 1) {
|
|
538
|
+
const op = parseInt(parts[1], 10);
|
|
539
|
+
if (Number.isFinite(op)) {
|
|
540
|
+
opacity = Math.max(0, Math.min(1, op / 100));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (colorGroup[token]) {
|
|
544
|
+
color = parseColor(colorGroup[token]);
|
|
545
|
+
} else if (isLikelyColorValue(token)) {
|
|
546
|
+
color = parseColor(token);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!sawRing) return null;
|
|
552
|
+
|
|
553
|
+
if (width == null) width = 3;
|
|
554
|
+
if (!color) {
|
|
555
|
+
if (colorGroup.ring) {
|
|
556
|
+
color = parseColor(colorGroup.ring);
|
|
557
|
+
} else if (colorGroup.primary) {
|
|
558
|
+
color = parseColor(colorGroup.primary);
|
|
559
|
+
} else {
|
|
560
|
+
color = parseColor('#16A34A');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (opacity == null) opacity = 1;
|
|
564
|
+
if (offsetWidth == null) offsetWidth = 0;
|
|
565
|
+
if (offsetWidth > 0 && !offsetColor) {
|
|
566
|
+
if (colorGroup.background) {
|
|
567
|
+
offsetColor = parseColor(colorGroup.background);
|
|
568
|
+
} else if (colorGroup.card) {
|
|
569
|
+
offsetColor = parseColor(colorGroup.card);
|
|
570
|
+
} else {
|
|
571
|
+
offsetColor = parseColor('#ffffff');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
width: width,
|
|
577
|
+
color: color,
|
|
578
|
+
opacity: opacity,
|
|
579
|
+
offsetWidth: offsetWidth,
|
|
580
|
+
offsetColor: offsetColor,
|
|
581
|
+
usedVariant: false,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function isRingUtility(value: string): boolean {
|
|
586
|
+
return value === 'ring'
|
|
587
|
+
|| value === 'ring-inset'
|
|
588
|
+
|| value.startsWith('ring-')
|
|
589
|
+
|| value.startsWith('ring-offset-');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function isLikelyColorValue(value: string): boolean {
|
|
593
|
+
if (value.toLowerCase().startsWith('oklch(')) return true;
|
|
594
|
+
return /^#([0-9a-f]{3}|([0-9a-f]{2}){3})$/i.test(value);
|
|
595
|
+
}
|