inkbridge 0.1.0-beta.1
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 +149 -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
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind Class Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses Tailwind CSS classes and extracts styling information
|
|
5
|
+
* for mapping to Figma properties.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ParsedTailwindClass, ColorMapping, SizeMapping } from './types';
|
|
9
|
+
import colors from 'tailwindcss/colors';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Responsive Breakpoints (all standard Tailwind breakpoints)
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export const RESPONSIVE_PREFIXES = [
|
|
16
|
+
'sm:',
|
|
17
|
+
'md:',
|
|
18
|
+
'lg:',
|
|
19
|
+
'xl:',
|
|
20
|
+
'2xl:',
|
|
21
|
+
'min-[', // arbitrary min-width: min-[320px]:
|
|
22
|
+
'max-sm:',
|
|
23
|
+
'max-md:',
|
|
24
|
+
'max-lg:',
|
|
25
|
+
'max-xl:',
|
|
26
|
+
'max-2xl:',
|
|
27
|
+
'max-[', // arbitrary max-width: max-[600px]:
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Pseudo-element / Pseudo-class / Structural Modifiers
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export const PSEUDO_MODIFIERS = [
|
|
35
|
+
// Pseudo-elements
|
|
36
|
+
'before:',
|
|
37
|
+
'after:',
|
|
38
|
+
'placeholder:',
|
|
39
|
+
'file:',
|
|
40
|
+
'selection:',
|
|
41
|
+
'marker:',
|
|
42
|
+
'backdrop:',
|
|
43
|
+
'first-line:',
|
|
44
|
+
'first-letter:',
|
|
45
|
+
// Structural pseudo-classes
|
|
46
|
+
'first:',
|
|
47
|
+
'last:',
|
|
48
|
+
'odd:',
|
|
49
|
+
'even:',
|
|
50
|
+
'only:',
|
|
51
|
+
'first-of-type:',
|
|
52
|
+
'last-of-type:',
|
|
53
|
+
'only-of-type:',
|
|
54
|
+
'empty:',
|
|
55
|
+
// Form pseudo-classes
|
|
56
|
+
'required:',
|
|
57
|
+
'optional:',
|
|
58
|
+
'valid:',
|
|
59
|
+
'invalid:',
|
|
60
|
+
'in-range:',
|
|
61
|
+
'out-of-range:',
|
|
62
|
+
'read-only:',
|
|
63
|
+
'read-write:',
|
|
64
|
+
'autofill:',
|
|
65
|
+
'checked:',
|
|
66
|
+
'indeterminate:',
|
|
67
|
+
'default:',
|
|
68
|
+
'enabled:',
|
|
69
|
+
// Print / Motion / Contrast
|
|
70
|
+
'print:',
|
|
71
|
+
'motion-safe:',
|
|
72
|
+
'motion-reduce:',
|
|
73
|
+
'contrast-more:',
|
|
74
|
+
'contrast-less:',
|
|
75
|
+
'forced-colors:',
|
|
76
|
+
// Direction / Language
|
|
77
|
+
'ltr:',
|
|
78
|
+
'rtl:',
|
|
79
|
+
// Dark mode
|
|
80
|
+
'dark:',
|
|
81
|
+
// Container queries
|
|
82
|
+
'@sm:',
|
|
83
|
+
'@md:',
|
|
84
|
+
'@lg:',
|
|
85
|
+
'@xl:',
|
|
86
|
+
'@2xl:',
|
|
87
|
+
// Supports / Has
|
|
88
|
+
'supports-[',
|
|
89
|
+
'has-[',
|
|
90
|
+
'not-[',
|
|
91
|
+
// Portrait / Landscape
|
|
92
|
+
'portrait:',
|
|
93
|
+
'landscape:',
|
|
94
|
+
// Open
|
|
95
|
+
'open:',
|
|
96
|
+
] as const;
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Tailwind Spacing Scale (default values in pixels, assuming 1rem = 16px)
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
export const TAILWIND_SPACING: Record<string, number> = {
|
|
103
|
+
'0': 0,
|
|
104
|
+
'px': 1,
|
|
105
|
+
'0.5': 2,
|
|
106
|
+
'1': 4,
|
|
107
|
+
'1.5': 6,
|
|
108
|
+
'2': 8,
|
|
109
|
+
'2.5': 10,
|
|
110
|
+
'3': 12,
|
|
111
|
+
'3.5': 14,
|
|
112
|
+
'4': 16,
|
|
113
|
+
'5': 20,
|
|
114
|
+
'6': 24,
|
|
115
|
+
'7': 28,
|
|
116
|
+
'8': 32,
|
|
117
|
+
'9': 36,
|
|
118
|
+
'10': 40,
|
|
119
|
+
'11': 44,
|
|
120
|
+
'12': 48,
|
|
121
|
+
'14': 56,
|
|
122
|
+
'16': 64,
|
|
123
|
+
'20': 80,
|
|
124
|
+
'24': 96,
|
|
125
|
+
'28': 112,
|
|
126
|
+
'32': 128,
|
|
127
|
+
'36': 144,
|
|
128
|
+
'40': 160,
|
|
129
|
+
'44': 176,
|
|
130
|
+
'48': 192,
|
|
131
|
+
'52': 208,
|
|
132
|
+
'56': 224,
|
|
133
|
+
'60': 240,
|
|
134
|
+
'64': 256,
|
|
135
|
+
'72': 288,
|
|
136
|
+
'80': 320,
|
|
137
|
+
'96': 384,
|
|
138
|
+
// Tailwind v4 named container sizes (rem × 16 → px)
|
|
139
|
+
'xs': 320,
|
|
140
|
+
'sm': 384,
|
|
141
|
+
'md': 448,
|
|
142
|
+
'lg': 512,
|
|
143
|
+
'xl': 576,
|
|
144
|
+
'2xl': 672,
|
|
145
|
+
'3xl': 768,
|
|
146
|
+
'4xl': 896,
|
|
147
|
+
'5xl': 1024,
|
|
148
|
+
'6xl': 1152,
|
|
149
|
+
'7xl': 1280,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a spacing token to pixels.
|
|
154
|
+
* Handles the v3 named scale, v4 named container sizes, and v4's open numeric
|
|
155
|
+
* scale where any integer n maps to n × 4 px (e.g. w-100 = 400 px).
|
|
156
|
+
*/
|
|
157
|
+
export function resolveSpacing(value: string): number | undefined {
|
|
158
|
+
const named = TAILWIND_SPACING[value];
|
|
159
|
+
if (named !== undefined) return named;
|
|
160
|
+
const n = parseFloat(value);
|
|
161
|
+
if (!Number.isNaN(n) && String(n) === value) return n * 4;
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// State Modifiers
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
export const STATE_MODIFIERS = [
|
|
170
|
+
'hover:',
|
|
171
|
+
'focus:',
|
|
172
|
+
'focus-visible:',
|
|
173
|
+
'focus-within:',
|
|
174
|
+
'active:',
|
|
175
|
+
'disabled:',
|
|
176
|
+
'aria-invalid:',
|
|
177
|
+
'aria-selected:',
|
|
178
|
+
'aria-expanded:',
|
|
179
|
+
'data-[state=open]:',
|
|
180
|
+
'data-[state=checked]:',
|
|
181
|
+
'data-[state=active]:',
|
|
182
|
+
'data-[disabled]:',
|
|
183
|
+
'group-hover:',
|
|
184
|
+
'group-focus:',
|
|
185
|
+
'peer-focus:',
|
|
186
|
+
'peer-disabled:',
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Parsing Functions
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* All known modifier prefixes (state, responsive, pseudo, dark, etc.)
|
|
195
|
+
* Sorted longest-first so that e.g. `focus-visible:` matches before `focus:`.
|
|
196
|
+
*/
|
|
197
|
+
const ALL_MODIFIERS: string[] = [
|
|
198
|
+
...STATE_MODIFIERS,
|
|
199
|
+
...RESPONSIVE_PREFIXES,
|
|
200
|
+
...PSEUDO_MODIFIERS,
|
|
201
|
+
].sort((a, b) => b.length - a.length);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Strip one modifier prefix from the front of a class string.
|
|
205
|
+
* Handles bracket-based modifiers like `data-[state=open]:`, `min-[320px]:`,
|
|
206
|
+
* `supports-[display:grid]:`, `has-[>img]:`, `not-[.disabled]:`, `max-[600px]:`.
|
|
207
|
+
* Returns the modifier string (including the trailing `:`) and the remainder,
|
|
208
|
+
* or null if no modifier is found.
|
|
209
|
+
*/
|
|
210
|
+
function stripOneModifier(cls: string): { modifier: string; rest: string } | null {
|
|
211
|
+
// 1. Try bracket-based modifiers first (data-[...]:, aria-[...]:, min-[...]:, etc.)
|
|
212
|
+
const bracketMatch = cls.match(/^([a-z@-]*\[[^\]]*\]:)(.*)/);
|
|
213
|
+
if (bracketMatch) {
|
|
214
|
+
return { modifier: bracketMatch[1], rest: bracketMatch[2] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 2. Try known fixed modifiers
|
|
218
|
+
for (const mod of ALL_MODIFIERS) {
|
|
219
|
+
if (cls.startsWith(mod)) {
|
|
220
|
+
return { modifier: mod, rest: cls.slice(mod.length) };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Parse a single Tailwind class into its components.
|
|
229
|
+
*
|
|
230
|
+
* Handles stacked modifiers (e.g. `sm:hover:bg-primary`, `dark:focus:ring-2`)
|
|
231
|
+
* and arbitrary bracket selectors (e.g. `data-[state=open]:animate-in`).
|
|
232
|
+
*/
|
|
233
|
+
export function parseTailwindClass(cls: string): ParsedTailwindClass {
|
|
234
|
+
const modifiers: string[] = [];
|
|
235
|
+
let utility = cls;
|
|
236
|
+
|
|
237
|
+
// Peel off all modifier layers
|
|
238
|
+
let result = stripOneModifier(utility);
|
|
239
|
+
while (result) {
|
|
240
|
+
modifiers.push(result.modifier);
|
|
241
|
+
utility = result.rest;
|
|
242
|
+
result = stripOneModifier(utility);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const modifier = modifiers.length > 0 ? modifiers.join('') : undefined;
|
|
246
|
+
|
|
247
|
+
// Categorise the modifiers
|
|
248
|
+
const responsive = modifiers.find(m =>
|
|
249
|
+
(RESPONSIVE_PREFIXES as readonly string[]).includes(m) || /^(min|max)-\[/.test(m)
|
|
250
|
+
);
|
|
251
|
+
const pseudo = modifiers.find(m =>
|
|
252
|
+
(PSEUDO_MODIFIERS as readonly string[]).includes(m)
|
|
253
|
+
);
|
|
254
|
+
const state = modifiers.find(m =>
|
|
255
|
+
STATE_MODIFIERS.includes(m)
|
|
256
|
+
);
|
|
257
|
+
const isDark = modifiers.includes('dark:');
|
|
258
|
+
|
|
259
|
+
// Handle opacity modifier in the utility (e.g. bg-muted/50)
|
|
260
|
+
let opacity: number | undefined;
|
|
261
|
+
const opacityMatch = utility.match(/^(.+)\/(\d+)$/);
|
|
262
|
+
if (opacityMatch) {
|
|
263
|
+
utility = opacityMatch[1];
|
|
264
|
+
opacity = parseInt(opacityMatch[2], 10);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Handle arbitrary values (e.g. text-[13px], left-[calc(50%-11rem)])
|
|
268
|
+
let arbitrary: string | undefined;
|
|
269
|
+
const arbitraryMatch = utility.match(/^([a-z-]+)-\[(.+)\]$/);
|
|
270
|
+
if (arbitraryMatch) {
|
|
271
|
+
arbitrary = arbitraryMatch[2];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle line-height slash notation (e.g. text-sm/6)
|
|
275
|
+
const slashNotation = cls.match(/^text-([a-z]+)\/(\d+)$/);
|
|
276
|
+
|
|
277
|
+
// Extract value from utility (e.g., "p-4" -> value = "4")
|
|
278
|
+
const match = utility.match(/^([a-z-]+)-(.+)$/);
|
|
279
|
+
const value = match?.[2];
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
original: cls,
|
|
283
|
+
modifier,
|
|
284
|
+
modifiers,
|
|
285
|
+
utility,
|
|
286
|
+
value,
|
|
287
|
+
responsive,
|
|
288
|
+
pseudo,
|
|
289
|
+
state,
|
|
290
|
+
isDark,
|
|
291
|
+
opacity,
|
|
292
|
+
arbitrary,
|
|
293
|
+
lineHeight: slashNotation ? slashNotation[2] : undefined,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parse a space-separated string of Tailwind classes
|
|
299
|
+
*/
|
|
300
|
+
export function parseClassString(classString: string): ParsedTailwindClass[] {
|
|
301
|
+
return classString
|
|
302
|
+
.split(/\s+/)
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.map(parseTailwindClass);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Extract color-related classes and map to token names
|
|
309
|
+
*/
|
|
310
|
+
export function extractColorMappings(classes: string[]): ColorMapping[] {
|
|
311
|
+
const mappings: ColorMapping[] = [];
|
|
312
|
+
|
|
313
|
+
for (const cls of classes) {
|
|
314
|
+
const parsed = parseTailwindClass(cls);
|
|
315
|
+
const utility = parsed.utility;
|
|
316
|
+
|
|
317
|
+
// Background colors
|
|
318
|
+
if (utility.startsWith('bg-')) {
|
|
319
|
+
const tokenName = utility.slice(3);
|
|
320
|
+
// Filter out non-token values like opacity modifiers
|
|
321
|
+
if (!tokenName.includes('/') && !tokenName.match(/^(transparent|current|inherit|white|black)$/)) {
|
|
322
|
+
mappings.push({
|
|
323
|
+
tailwindClass: cls,
|
|
324
|
+
tokenName,
|
|
325
|
+
property: 'background',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Text colors
|
|
331
|
+
if (utility.startsWith('text-') && !utility.match(/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/)) {
|
|
332
|
+
const tokenName = utility.slice(5);
|
|
333
|
+
if (!tokenName.includes('/') && !tokenName.match(/^(transparent|current|inherit|white|black)$/)) {
|
|
334
|
+
mappings.push({
|
|
335
|
+
tailwindClass: cls,
|
|
336
|
+
tokenName,
|
|
337
|
+
property: 'text',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Border colors
|
|
343
|
+
if (utility.startsWith('border-') && !utility.match(/^border-(\d+|t|r|b|l|x|y)$/)) {
|
|
344
|
+
const tokenName = utility.slice(7);
|
|
345
|
+
if (!tokenName.includes('/') && !tokenName.match(/^(transparent|current|inherit|white|black)$/)) {
|
|
346
|
+
mappings.push({
|
|
347
|
+
tailwindClass: cls,
|
|
348
|
+
tokenName,
|
|
349
|
+
property: 'border',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Ring colors
|
|
355
|
+
if (utility.startsWith('ring-') && !utility.match(/^ring-(\d+|inset|offset)$/)) {
|
|
356
|
+
const tokenName = utility.slice(5);
|
|
357
|
+
if (!tokenName.includes('/') && !tokenName.match(/^(transparent|current|inherit|white|black)$/)) {
|
|
358
|
+
mappings.push({
|
|
359
|
+
tailwindClass: cls,
|
|
360
|
+
tokenName,
|
|
361
|
+
property: 'ring',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return mappings;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Extract Tailwind palette tokens used in classes and map to color values.
|
|
372
|
+
*/
|
|
373
|
+
export function extractPaletteTokens(classes: string[]): Record<string, string> {
|
|
374
|
+
const palette: Record<string, string> = {};
|
|
375
|
+
const prefixes = ['bg-', 'text-', 'border-', 'ring-', 'from-', 'to-', 'via-', 'fill-', 'stroke-'];
|
|
376
|
+
|
|
377
|
+
const resolvePaletteColor = (token: string): string | null => {
|
|
378
|
+
if (!token) return null;
|
|
379
|
+
if (token === 'transparent' || token === 'current' || token === 'inherit') return null;
|
|
380
|
+
const direct = (colors as any)[token];
|
|
381
|
+
if (typeof direct === 'string') return direct;
|
|
382
|
+
const parts = token.split('-');
|
|
383
|
+
if (parts.length < 2) return null;
|
|
384
|
+
const shade = parts[parts.length - 1];
|
|
385
|
+
const name = parts.slice(0, -1).join('-');
|
|
386
|
+
const group = (colors as any)[name];
|
|
387
|
+
if (group && typeof group === 'object' && group[shade]) return group[shade];
|
|
388
|
+
return null;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
for (const cls of classes) {
|
|
392
|
+
const parsed = parseTailwindClass(cls);
|
|
393
|
+
const utility = parsed.utility;
|
|
394
|
+
for (const prefix of prefixes) {
|
|
395
|
+
if (!utility.startsWith(prefix)) continue;
|
|
396
|
+
const token = utility.slice(prefix.length);
|
|
397
|
+
if (!token || token.startsWith('[')) break;
|
|
398
|
+
const value = resolvePaletteColor(token);
|
|
399
|
+
if (value) palette[token] = value;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return palette;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Extract size-related classes and convert to pixel values
|
|
409
|
+
*/
|
|
410
|
+
export function extractSizeMappings(classes: string[]): SizeMapping[] {
|
|
411
|
+
const mappings: SizeMapping[] = [];
|
|
412
|
+
|
|
413
|
+
for (const cls of classes) {
|
|
414
|
+
const parsed = parseTailwindClass(cls);
|
|
415
|
+
const utility = parsed.utility;
|
|
416
|
+
|
|
417
|
+
// Height
|
|
418
|
+
if (utility.startsWith('h-')) {
|
|
419
|
+
const value = utility.slice(2);
|
|
420
|
+
const px = resolveSpacing(value);
|
|
421
|
+
if (px !== undefined) {
|
|
422
|
+
mappings.push({ tailwindClass: cls, property: 'height', value: px });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Width
|
|
427
|
+
if (utility.startsWith('w-')) {
|
|
428
|
+
const value = utility.slice(2);
|
|
429
|
+
const px = resolveSpacing(value);
|
|
430
|
+
if (px !== undefined) {
|
|
431
|
+
mappings.push({ tailwindClass: cls, property: 'width', value: px });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Padding (all sides)
|
|
436
|
+
if (utility.startsWith('p-') && !utility.startsWith('px-') && !utility.startsWith('py-') && !utility.startsWith('pt-') && !utility.startsWith('pr-') && !utility.startsWith('pb-') && !utility.startsWith('pl-')) {
|
|
437
|
+
const value = utility.slice(2);
|
|
438
|
+
const px = resolveSpacing(value);
|
|
439
|
+
if (px !== undefined) {
|
|
440
|
+
mappings.push({ tailwindClass: cls, property: 'padding', value: px });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Padding X
|
|
445
|
+
if (utility.startsWith('px-')) {
|
|
446
|
+
const value = utility.slice(3);
|
|
447
|
+
const px = resolveSpacing(value);
|
|
448
|
+
if (px !== undefined) {
|
|
449
|
+
mappings.push({ tailwindClass: cls, property: 'padding', value: px });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Padding Y
|
|
454
|
+
if (utility.startsWith('py-')) {
|
|
455
|
+
const value = utility.slice(3);
|
|
456
|
+
const px = resolveSpacing(value);
|
|
457
|
+
if (px !== undefined) {
|
|
458
|
+
mappings.push({ tailwindClass: cls, property: 'padding', value: px });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Gap
|
|
463
|
+
if (utility.startsWith('gap-')) {
|
|
464
|
+
const value = utility.slice(4);
|
|
465
|
+
const px = resolveSpacing(value);
|
|
466
|
+
if (px !== undefined) {
|
|
467
|
+
mappings.push({ tailwindClass: cls, property: 'gap', value: px });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Space-y, space-x (item spacing)
|
|
472
|
+
if (utility.startsWith('space-y-') || utility.startsWith('space-x-')) {
|
|
473
|
+
const value = utility.slice(8);
|
|
474
|
+
const px = resolveSpacing(value);
|
|
475
|
+
if (px !== undefined) {
|
|
476
|
+
mappings.push({ tailwindClass: cls, property: 'gap', value: px });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return mappings;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Group classes by their state modifier
|
|
486
|
+
*/
|
|
487
|
+
export function groupClassesByState(classes: string[]): Record<string, string[]> {
|
|
488
|
+
const groups: Record<string, string[]> = {
|
|
489
|
+
default: [],
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
for (const cls of classes) {
|
|
493
|
+
const parsed = parseTailwindClass(cls);
|
|
494
|
+
|
|
495
|
+
if (parsed.state) {
|
|
496
|
+
// Normalize modifier to state name
|
|
497
|
+
let stateName = 'default';
|
|
498
|
+
if (parsed.state.includes('hover')) stateName = 'hover';
|
|
499
|
+
else if (parsed.state.includes('focus')) stateName = 'focus';
|
|
500
|
+
else if (parsed.state.includes('disabled') || parsed.state.includes('[disabled]')) stateName = 'disabled';
|
|
501
|
+
else if (parsed.state.includes('aria-invalid')) stateName = 'error';
|
|
502
|
+
else if (parsed.state.includes('active')) stateName = 'active';
|
|
503
|
+
else if (parsed.state.includes('checked')) stateName = 'checked';
|
|
504
|
+
else if (parsed.state.includes('open')) stateName = 'open';
|
|
505
|
+
|
|
506
|
+
if (!groups[stateName]) {
|
|
507
|
+
groups[stateName] = [];
|
|
508
|
+
}
|
|
509
|
+
groups[stateName].push(parsed.utility);
|
|
510
|
+
} else if (!parsed.modifier) {
|
|
511
|
+
groups.default.push(cls);
|
|
512
|
+
} else {
|
|
513
|
+
// Classes with only responsive/pseudo/dark modifiers go to default
|
|
514
|
+
groups.default.push(cls);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return groups;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Group classes by responsive breakpoint
|
|
523
|
+
*/
|
|
524
|
+
export function groupClassesByBreakpoint(classes: string[]): Record<string, string[]> {
|
|
525
|
+
const groups: Record<string, string[]> = {
|
|
526
|
+
base: [],
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
for (const cls of classes) {
|
|
530
|
+
const parsed = parseTailwindClass(cls);
|
|
531
|
+
|
|
532
|
+
if (parsed.responsive) {
|
|
533
|
+
const bp = parsed.responsive.replace(/:$/, '');
|
|
534
|
+
if (!groups[bp]) groups[bp] = [];
|
|
535
|
+
groups[bp].push(parsed.utility);
|
|
536
|
+
} else {
|
|
537
|
+
groups.base.push(cls);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return groups;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Separate dark mode classes from light mode classes
|
|
546
|
+
*/
|
|
547
|
+
export function groupClassesByColorScheme(classes: string[]): { light: string[]; dark: string[] } {
|
|
548
|
+
const light: string[] = [];
|
|
549
|
+
const dark: string[] = [];
|
|
550
|
+
|
|
551
|
+
for (const cls of classes) {
|
|
552
|
+
const parsed = parseTailwindClass(cls);
|
|
553
|
+
if (parsed.isDark) {
|
|
554
|
+
dark.push(parsed.utility);
|
|
555
|
+
} else {
|
|
556
|
+
light.push(cls);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { light, dark };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Infer layout properties from a set of Tailwind classes
|
|
565
|
+
*/
|
|
566
|
+
export function inferLayout(classes: string[]): import('./types').LayoutInfo {
|
|
567
|
+
const layout: import('./types').LayoutInfo = {
|
|
568
|
+
display: null,
|
|
569
|
+
direction: null,
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
for (const cls of classes) {
|
|
573
|
+
const parsed = parseTailwindClass(cls);
|
|
574
|
+
// Only look at base (non-responsive, non-state) classes for primary layout
|
|
575
|
+
if (parsed.modifier) continue;
|
|
576
|
+
const u = parsed.utility;
|
|
577
|
+
|
|
578
|
+
// Display
|
|
579
|
+
if (u === 'flex') layout.display = 'flex';
|
|
580
|
+
else if (u === 'grid') layout.display = 'grid';
|
|
581
|
+
else if (u === 'block') layout.display = 'block';
|
|
582
|
+
else if (u === 'inline' || u === 'inline-flex' || u === 'inline-block') layout.display = 'inline';
|
|
583
|
+
else if (u === 'hidden') layout.display = 'hidden';
|
|
584
|
+
else if (u === 'table') layout.display = 'table';
|
|
585
|
+
else if (u === 'contents') layout.display = 'contents';
|
|
586
|
+
|
|
587
|
+
// Direction
|
|
588
|
+
if (u === 'flex-row') layout.direction = 'row';
|
|
589
|
+
else if (u === 'flex-col') layout.direction = 'col';
|
|
590
|
+
|
|
591
|
+
// Wrap
|
|
592
|
+
if (u === 'flex-wrap') layout.wrap = true;
|
|
593
|
+
else if (u === 'flex-nowrap') layout.wrap = false;
|
|
594
|
+
|
|
595
|
+
// Gap
|
|
596
|
+
if (u.startsWith('gap-')) {
|
|
597
|
+
const val = u.slice(4);
|
|
598
|
+
const px = resolveSpacing(val);
|
|
599
|
+
if (px !== undefined) layout.gap = px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Padding
|
|
603
|
+
if (u.startsWith('p-') && !u.startsWith('px-') && !u.startsWith('py-') && !u.startsWith('pt-') && !u.startsWith('pb-') && !u.startsWith('pl-') && !u.startsWith('pr-') && !u.startsWith('ps-') && !u.startsWith('pe-')) {
|
|
604
|
+
const val = u.slice(2);
|
|
605
|
+
const px = resolveSpacing(val);
|
|
606
|
+
if (px !== undefined) layout.padding = { all: px };
|
|
607
|
+
}
|
|
608
|
+
if (u.startsWith('px-')) {
|
|
609
|
+
const val = u.slice(3);
|
|
610
|
+
const px = resolveSpacing(val);
|
|
611
|
+
if (px !== undefined) layout.padding = { ...layout.padding, x: px };
|
|
612
|
+
}
|
|
613
|
+
if (u.startsWith('py-')) {
|
|
614
|
+
const val = u.slice(3);
|
|
615
|
+
const px = resolveSpacing(val);
|
|
616
|
+
if (px !== undefined) layout.padding = { ...layout.padding, y: px };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Alignment
|
|
620
|
+
if (u.startsWith('items-')) layout.alignment = u.slice(6);
|
|
621
|
+
if (u.startsWith('justify-')) layout.justification = u.slice(8);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Default flex direction to row if display is flex and no explicit direction
|
|
625
|
+
if (layout.display === 'flex' && !layout.direction) {
|
|
626
|
+
layout.direction = 'row';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return layout;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Extract all unique color tokens used in a set of classes
|
|
634
|
+
*/
|
|
635
|
+
export function extractColorTokens(classes: string[]): string[] {
|
|
636
|
+
const tokens = new Set<string>();
|
|
637
|
+
const colorMappings = extractColorMappings(classes);
|
|
638
|
+
|
|
639
|
+
for (const mapping of colorMappings) {
|
|
640
|
+
tokens.add(mapping.tokenName);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return Array.from(tokens);
|
|
644
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
centerPlacedRotationTransform,
|
|
4
|
+
rotationTransformAroundPointRadians,
|
|
5
|
+
} from '../src/transform-math';
|
|
6
|
+
|
|
7
|
+
function approxEqual(actual: number, expected: number, epsilon = 0.0001): void {
|
|
8
|
+
assert.ok(Math.abs(actual - expected) <= epsilon, `expected ${expected}, got ${actual}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertMatrix(
|
|
12
|
+
actual: [[number, number, number], [number, number, number]],
|
|
13
|
+
expected: [[number, number, number], [number, number, number]],
|
|
14
|
+
): void {
|
|
15
|
+
approxEqual(actual[0][0], expected[0][0]);
|
|
16
|
+
approxEqual(actual[0][1], expected[0][1]);
|
|
17
|
+
approxEqual(actual[0][2], expected[0][2]);
|
|
18
|
+
approxEqual(actual[1][0], expected[1][0]);
|
|
19
|
+
approxEqual(actual[1][1], expected[1][1]);
|
|
20
|
+
approxEqual(actual[1][2], expected[1][2]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// For 45° around normalized center, this should match previous hardcoded
|
|
24
|
+
// Tailwind gradient direction matrix semantics.
|
|
25
|
+
const angle = Math.PI / 4;
|
|
26
|
+
const rotateCenter = rotationTransformAroundPointRadians(angle, 0.5, 0.5);
|
|
27
|
+
const cos = Math.cos(angle);
|
|
28
|
+
const sin = Math.sin(angle);
|
|
29
|
+
assertMatrix(rotateCenter, [
|
|
30
|
+
[cos, -sin, 0.5 * (1 - cos + sin)],
|
|
31
|
+
[sin, cos, 0.5 * (1 - sin - cos)],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// Hero reference case: width=1155, height=678, center=(32,339), rotation=30deg.
|
|
35
|
+
// We keep this as a regression lock for blob center placement.
|
|
36
|
+
const blobTransform = centerPlacedRotationTransform(1155, 678, 32, 339, 30);
|
|
37
|
+
assertMatrix(blobTransform, [
|
|
38
|
+
[Math.cos(Math.PI / 6), -Math.sin(Math.PI / 6), -298.62967068551336],
|
|
39
|
+
[Math.sin(Math.PI / 6), Math.cos(Math.PI / 6), -243.33261188292465],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
console.log('transform math regression passed');
|