inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
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 +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutIR — the intermediate representation produced by the Tailwind →
|
|
3
|
+
* Figma layout parser. The split parsers (flex, grid, spacing, alignment,
|
|
4
|
+
* sizing, responsive) all read class lists and write into a single LayoutIR
|
|
5
|
+
* instance via small `parseX(classes, ir)` helpers. The orchestrator in
|
|
6
|
+
* `layout-parser.ts` (`LayoutParser.parseToIR`) calls them in order and
|
|
7
|
+
* hands the result to `LayoutParser.applyToFrame`.
|
|
8
|
+
*
|
|
9
|
+
* This file owns:
|
|
10
|
+
* - the `LayoutIR` shape itself
|
|
11
|
+
* - the `SizingMode` enum reused on width / height axes
|
|
12
|
+
* - the IR factory `makeEmptyIR()` so each parser starts from a known
|
|
13
|
+
* baseline rather than each call site repeating the defaults
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sizing mode for a single axis (width or height).
|
|
18
|
+
* - FIXED: explicit pixel size (w-20, h-10)
|
|
19
|
+
* - HUG: shrink to content (default for text, no size class)
|
|
20
|
+
* - FILL: expand to fill available space (flex-1, w-full inside a flex parent)
|
|
21
|
+
*/
|
|
22
|
+
export type SizingMode = 'FIXED' | 'HUG' | 'FILL';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Captures the Tailwind layout intent before any Figma node is touched.
|
|
26
|
+
*/
|
|
27
|
+
export interface LayoutIR {
|
|
28
|
+
// Container properties
|
|
29
|
+
layoutMode: 'HORIZONTAL' | 'VERTICAL' | 'NONE';
|
|
30
|
+
wrap: boolean;
|
|
31
|
+
gap: number;
|
|
32
|
+
gapX?: number;
|
|
33
|
+
gapY?: number;
|
|
34
|
+
|
|
35
|
+
// Padding
|
|
36
|
+
paddingTop: number;
|
|
37
|
+
paddingRight: number;
|
|
38
|
+
paddingBottom: number;
|
|
39
|
+
paddingLeft: number;
|
|
40
|
+
|
|
41
|
+
// Alignment (for containers)
|
|
42
|
+
mainAlign: 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN';
|
|
43
|
+
crossAlign: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | 'BASELINE';
|
|
44
|
+
|
|
45
|
+
// Sizing (resolved based on context)
|
|
46
|
+
widthMode: SizingMode;
|
|
47
|
+
heightMode: SizingMode;
|
|
48
|
+
fixedWidth?: number;
|
|
49
|
+
fixedHeight?: number;
|
|
50
|
+
widthFraction?: number; // 0.0-1.0 for fractional widths (w-1/2, w-1/3, etc.)
|
|
51
|
+
heightFraction?: number; // 0.0-1.0 for fractional heights
|
|
52
|
+
minWidth?: number; // pixel floor from `min-w-N` / `min-w-[Npx]`
|
|
53
|
+
minHeight?: number; // pixel floor from `min-h-N` / `min-h-[Npx]`
|
|
54
|
+
|
|
55
|
+
// Child-specific (applied when this node is a child of a flex parent)
|
|
56
|
+
grow: number; // 0 = don't grow, 1 = grow
|
|
57
|
+
shrinkZero?: boolean; // `shrink-0` / `flex-shrink-0` — force layoutGrow = 0
|
|
58
|
+
selfAlign?: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Default IR — every parser-side function mutates this in place rather
|
|
63
|
+
* than constructing partial objects, so the shape stays consistent.
|
|
64
|
+
*/
|
|
65
|
+
export function makeEmptyIR(): LayoutIR {
|
|
66
|
+
return {
|
|
67
|
+
layoutMode: 'NONE',
|
|
68
|
+
wrap: false,
|
|
69
|
+
gap: 0,
|
|
70
|
+
paddingTop: 0,
|
|
71
|
+
paddingRight: 0,
|
|
72
|
+
paddingBottom: 0,
|
|
73
|
+
paddingLeft: 0,
|
|
74
|
+
mainAlign: 'MIN',
|
|
75
|
+
crossAlign: 'MIN',
|
|
76
|
+
widthMode: 'HUG',
|
|
77
|
+
heightMode: 'HUG',
|
|
78
|
+
grow: 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect the LayoutIR `layoutMode` from Tailwind display + direction
|
|
3
|
+
* utilities. Spans flex AND grid because both end up writing to the
|
|
4
|
+
* same IR field — Figma auto-layout has only HORIZONTAL / VERTICAL /
|
|
5
|
+
* NONE, with no separate flex/grid distinction.
|
|
6
|
+
*
|
|
7
|
+
* Inputs handled:
|
|
8
|
+
* - `flex` / `inline-flex` → HORIZONTAL (CSS default flex-direction: row)
|
|
9
|
+
* - `flex-col` / `flex-col-reverse` → VERTICAL
|
|
10
|
+
* - `flex-row` / `flex-row-reverse` → HORIZONTAL
|
|
11
|
+
* - `grid` / `inline-grid` → VERTICAL by default; HORIZONTAL when a
|
|
12
|
+
* `grid-cols-N` class is also present (the columns wrap children
|
|
13
|
+
* horizontally; the actual column count is applied later by
|
|
14
|
+
* `markGridColumnsNode`, not in the IR itself)
|
|
15
|
+
*
|
|
16
|
+
* Class-order semantics matter: a later utility overrides earlier ones,
|
|
17
|
+
* which is what makes responsive variants like
|
|
18
|
+
* `flex flex-col sm:flex-row` work — the `sm:` prefix is stripped at the
|
|
19
|
+
* breakpoint resolver, leaving `flex flex-col flex-row` at the `sm`
|
|
20
|
+
* pass. Trailing `flex-row` then wins.
|
|
21
|
+
*
|
|
22
|
+
* Behaviour preserved 1:1 from the original `parseLayoutMode` static
|
|
23
|
+
* method on `LayoutParser`. Locked in by the layout-mode regression test.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export function parseLayoutMode(classes: string[]): 'HORIZONTAL' | 'VERTICAL' | 'NONE' {
|
|
27
|
+
let mode: 'HORIZONTAL' | 'VERTICAL' | 'NONE' = 'NONE';
|
|
28
|
+
// Track whether flex-direction was explicitly set: in real CSS,
|
|
29
|
+
// `display:flex` does not reset `flex-direction`, so a trailing bare
|
|
30
|
+
// `flex` after `flex-col` must not override the direction back to row.
|
|
31
|
+
let directionSet = false;
|
|
32
|
+
for (const cls of classes) {
|
|
33
|
+
if (cls === 'flex' || cls === 'inline-flex') {
|
|
34
|
+
if (!directionSet) mode = 'HORIZONTAL'; // default flex direction is row
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (cls === 'grid' || cls === 'inline-grid') {
|
|
38
|
+
// grid without grid-cols-N is a single-column layout (like flex-col).
|
|
39
|
+
// grid WITH grid-cols-N wraps horizontally — the column count is
|
|
40
|
+
// applied later by markGridColumnsNode, not stored in the IR.
|
|
41
|
+
const hasColumns = classes.some(c => /^grid-cols-\d+$/.test(c));
|
|
42
|
+
mode = hasColumns ? 'HORIZONTAL' : 'VERTICAL';
|
|
43
|
+
directionSet = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (cls === 'flex-col' || cls === 'flex-col-reverse') {
|
|
47
|
+
mode = 'VERTICAL';
|
|
48
|
+
directionSet = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (cls === 'flex-row' || cls === 'flex-row-reverse') {
|
|
52
|
+
mode = 'HORIZONTAL';
|
|
53
|
+
directionSet = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return mode;
|
|
57
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind sizing utilities → LayoutIR.
|
|
3
|
+
* Owns: w-*, h-*, size-*, w-full, h-full, h-screen, min-h-screen,
|
|
4
|
+
* fractional w/h (w-1/2, w-2/3, h-1/3, …), arbitrary `[Xpx/rem/em]`,
|
|
5
|
+
* named max-w (xs, sm, …, 7xl, prose, screen-*).
|
|
6
|
+
*
|
|
7
|
+
* Behaviour preserved 1:1 from the original `parseSizing` static method
|
|
8
|
+
* on `LayoutParser`. Locked in by the sizing regression test.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { LayoutIR } from './ir';
|
|
12
|
+
import { resolveSpacing } from './spacing-scale';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tailwind fractional width/height utilities — map to a 0..1 fraction.
|
|
16
|
+
* In Figma we materialize fractions as proportional grow on the parent's
|
|
17
|
+
* primary axis (see `parseSizing` setting `widthMode: 'FILL'` + `widthFraction`).
|
|
18
|
+
*/
|
|
19
|
+
const FRACTIONAL_SIZES: Record<string, number> = {
|
|
20
|
+
// Halves
|
|
21
|
+
'1/2': 0.5,
|
|
22
|
+
// Thirds
|
|
23
|
+
'1/3': 1 / 3,
|
|
24
|
+
'2/3': 2 / 3,
|
|
25
|
+
// Quarters
|
|
26
|
+
'1/4': 0.25,
|
|
27
|
+
'2/4': 0.5,
|
|
28
|
+
'3/4': 0.75,
|
|
29
|
+
// Fifths
|
|
30
|
+
'1/5': 0.2,
|
|
31
|
+
'2/5': 0.4,
|
|
32
|
+
'3/5': 0.6,
|
|
33
|
+
'4/5': 0.8,
|
|
34
|
+
// Sixths
|
|
35
|
+
'1/6': 1 / 6,
|
|
36
|
+
'2/6': 2 / 6,
|
|
37
|
+
'3/6': 0.5,
|
|
38
|
+
'4/6': 4 / 6,
|
|
39
|
+
'5/6': 5 / 6,
|
|
40
|
+
// Twelfths
|
|
41
|
+
'1/12': 1 / 12,
|
|
42
|
+
'2/12': 2 / 12,
|
|
43
|
+
'3/12': 0.25,
|
|
44
|
+
'4/12': 4 / 12,
|
|
45
|
+
'5/12': 5 / 12,
|
|
46
|
+
'6/12': 0.5,
|
|
47
|
+
'7/12': 7 / 12,
|
|
48
|
+
'8/12': 8 / 12,
|
|
49
|
+
'9/12': 0.75,
|
|
50
|
+
'10/12': 10 / 12,
|
|
51
|
+
'11/12': 11 / 12,
|
|
52
|
+
// Full
|
|
53
|
+
'full': 1.0,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tailwind named max-width tokens (default theme).
|
|
58
|
+
* Treated as fixed widths — Figma has no "max" constraint that maps
|
|
59
|
+
* cleanly onto a frame, so the closest faithful render is to pin the
|
|
60
|
+
* frame at the named max value.
|
|
61
|
+
*/
|
|
62
|
+
const NAMED_MAX_WIDTHS: Record<string, number> = {
|
|
63
|
+
'xs': 320, 'sm': 384, 'md': 448, 'lg': 512, 'xl': 576,
|
|
64
|
+
'2xl': 672, '3xl': 768, '4xl': 896, '5xl': 1024, '6xl': 1152, '7xl': 1280,
|
|
65
|
+
'prose': 640,
|
|
66
|
+
'screen-sm': 640, 'screen-md': 768, 'screen-lg': 1024, 'screen-xl': 1280, 'screen-2xl': 1536,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse sizing utilities into `ir.widthMode/heightMode/fixedWidth/
|
|
71
|
+
* fixedHeight/widthFraction/heightFraction`. Mutates the IR in place.
|
|
72
|
+
*/
|
|
73
|
+
export function parseSizing(classes: string[], ir: LayoutIR): void {
|
|
74
|
+
for (const cls of classes) {
|
|
75
|
+
// Fixed width: w-20 (scale), w-[100px] (arbitrary)
|
|
76
|
+
const wScaleMatch = cls.match(/^w-(\d+(?:\.\d+)?)$/);
|
|
77
|
+
if (wScaleMatch) {
|
|
78
|
+
const val = resolveSpacing(wScaleMatch[1]);
|
|
79
|
+
if (val > 0) {
|
|
80
|
+
ir.widthMode = 'FIXED';
|
|
81
|
+
ir.fixedWidth = val;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const wArbitraryMatch = cls.match(/^w-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
|
|
87
|
+
if (wArbitraryMatch) {
|
|
88
|
+
let val = parseFloat(wArbitraryMatch[1]);
|
|
89
|
+
const unit = wArbitraryMatch[2] || 'px';
|
|
90
|
+
if (unit === 'rem' || unit === 'em') val *= 16;
|
|
91
|
+
ir.widthMode = 'FIXED';
|
|
92
|
+
ir.fixedWidth = val;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Arbitrary percentage width: `w-[60%]` → parent-relative fraction.
|
|
97
|
+
// Used by the shadcn Progress adapter (indicator inside track) and by
|
|
98
|
+
// any consumer who writes percent widths directly in className.
|
|
99
|
+
const wPercentMatch = cls.match(/^w-\[(\d+(?:\.\d+)?)%\]$/);
|
|
100
|
+
if (wPercentMatch) {
|
|
101
|
+
const pct = parseFloat(wPercentMatch[1]);
|
|
102
|
+
if (Number.isFinite(pct) && pct >= 0) {
|
|
103
|
+
ir.widthMode = 'FILL';
|
|
104
|
+
ir.widthFraction = Math.max(0, Math.min(1, pct / 100));
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fractional width: w-1/2, w-1/3, w-2/3, etc.
|
|
110
|
+
const wFractionMatch = cls.match(/^w-(\d+\/\d+)$/);
|
|
111
|
+
if (wFractionMatch) {
|
|
112
|
+
const fraction = FRACTIONAL_SIZES[wFractionMatch[1]];
|
|
113
|
+
if (fraction !== undefined) {
|
|
114
|
+
ir.widthMode = 'FILL';
|
|
115
|
+
ir.widthFraction = fraction;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fixed height: h-20 (scale), h-[100px] (arbitrary)
|
|
121
|
+
const hScaleMatch = cls.match(/^h-(\d+(?:\.\d+)?)$/);
|
|
122
|
+
if (hScaleMatch) {
|
|
123
|
+
const val = resolveSpacing(hScaleMatch[1]);
|
|
124
|
+
if (val > 0) {
|
|
125
|
+
ir.heightMode = 'FIXED';
|
|
126
|
+
ir.fixedHeight = val;
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hArbitraryMatch = cls.match(/^h-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
|
|
132
|
+
if (hArbitraryMatch) {
|
|
133
|
+
let val = parseFloat(hArbitraryMatch[1]);
|
|
134
|
+
const unit = hArbitraryMatch[2] || 'px';
|
|
135
|
+
if (unit === 'rem' || unit === 'em') val *= 16;
|
|
136
|
+
ir.heightMode = 'FIXED';
|
|
137
|
+
ir.fixedHeight = val;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Arbitrary percentage height: `h-[40%]` — mirror of `w-[N%]`.
|
|
142
|
+
const hPercentMatch = cls.match(/^h-\[(\d+(?:\.\d+)?)%\]$/);
|
|
143
|
+
if (hPercentMatch) {
|
|
144
|
+
const pct = parseFloat(hPercentMatch[1]);
|
|
145
|
+
if (Number.isFinite(pct) && pct >= 0) {
|
|
146
|
+
ir.heightMode = 'FILL';
|
|
147
|
+
ir.heightFraction = Math.max(0, Math.min(1, pct / 100));
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fractional height: h-1/2, h-1/3, h-2/3, etc.
|
|
153
|
+
const hFractionMatch = cls.match(/^h-(\d+\/\d+)$/);
|
|
154
|
+
if (hFractionMatch) {
|
|
155
|
+
const fraction = FRACTIONAL_SIZES[hFractionMatch[1]];
|
|
156
|
+
if (fraction !== undefined) {
|
|
157
|
+
ir.heightMode = 'FILL';
|
|
158
|
+
ir.heightFraction = fraction;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Size (both dimensions): size-20
|
|
164
|
+
const sizeMatch = cls.match(/^size-(\d+(?:\.\d+)?)$/);
|
|
165
|
+
if (sizeMatch) {
|
|
166
|
+
const val = resolveSpacing(sizeMatch[1]);
|
|
167
|
+
if (val > 0) {
|
|
168
|
+
ir.widthMode = 'FIXED';
|
|
169
|
+
ir.heightMode = 'FIXED';
|
|
170
|
+
ir.fixedWidth = val;
|
|
171
|
+
ir.fixedHeight = val;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Fill width / height (resolved later in applyChildProperties).
|
|
177
|
+
if (cls === 'w-full') {
|
|
178
|
+
ir.widthMode = 'FILL';
|
|
179
|
+
}
|
|
180
|
+
if (cls === 'h-full' || cls === 'h-screen' || cls === 'min-h-screen') {
|
|
181
|
+
// h-screen / min-h-screen behave like h-full once resolveStoryLayoutHeight
|
|
182
|
+
// pins the story frame's primary axis to a fixed viewport height.
|
|
183
|
+
ir.heightMode = 'FILL';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Min-height (scale): `min-h-20` → 80px floor on frame height.
|
|
187
|
+
// Used by shadcn Textarea (`min-h-20`), tall hero containers, etc.
|
|
188
|
+
const minHScaleMatch = cls.match(/^min-h-(\d+(?:\.\d+)?)$/);
|
|
189
|
+
if (minHScaleMatch) {
|
|
190
|
+
const val = resolveSpacing(minHScaleMatch[1]);
|
|
191
|
+
if (val > 0) ir.minHeight = val;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const minHArbitraryMatch = cls.match(/^min-h-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
|
|
195
|
+
if (minHArbitraryMatch) {
|
|
196
|
+
let val = parseFloat(minHArbitraryMatch[1]);
|
|
197
|
+
const unit = minHArbitraryMatch[2] || 'px';
|
|
198
|
+
if (unit === 'rem' || unit === 'em') val *= 16;
|
|
199
|
+
if (val > 0) ir.minHeight = val;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Min-width — mirror of min-h-*.
|
|
204
|
+
const minWScaleMatch = cls.match(/^min-w-(\d+(?:\.\d+)?)$/);
|
|
205
|
+
if (minWScaleMatch) {
|
|
206
|
+
const val = resolveSpacing(minWScaleMatch[1]);
|
|
207
|
+
if (val > 0) ir.minWidth = val;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const minWArbitraryMatch = cls.match(/^min-w-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
|
|
211
|
+
if (minWArbitraryMatch) {
|
|
212
|
+
let val = parseFloat(minWArbitraryMatch[1]);
|
|
213
|
+
const unit = minWArbitraryMatch[2] || 'px';
|
|
214
|
+
if (unit === 'rem' || unit === 'em') val *= 16;
|
|
215
|
+
if (val > 0) ir.minWidth = val;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Max-width with fixed scale value
|
|
220
|
+
const maxWMatch = cls.match(/^max-w-(\d+(?:\.\d+)?)$/);
|
|
221
|
+
if (maxWMatch) {
|
|
222
|
+
const val = resolveSpacing(maxWMatch[1]);
|
|
223
|
+
if (val > 0) {
|
|
224
|
+
ir.widthMode = 'FIXED';
|
|
225
|
+
ir.fixedWidth = val;
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Named max-width (max-w-xl, max-w-2xl, …)
|
|
231
|
+
const maxWNamedMatch = cls.match(/^max-w-(xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|full|prose|screen-sm|screen-md|screen-lg|screen-xl|screen-2xl)$/);
|
|
232
|
+
if (maxWNamedMatch) {
|
|
233
|
+
const val = NAMED_MAX_WIDTHS[maxWNamedMatch[1]];
|
|
234
|
+
if (val) {
|
|
235
|
+
ir.widthMode = 'FIXED';
|
|
236
|
+
ir.fixedWidth = val;
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind spacing scale (in px) and the resolver function.
|
|
3
|
+
* Shared by the spacing parser (gap, padding) and the sizing parser
|
|
4
|
+
* (w-*, h-* in scale units). Pure: no Figma dependency, no IR mutation.
|
|
5
|
+
*
|
|
6
|
+
* Based on Tailwind v3+ docs: 1rem = 16px, scale base unit 0.25rem (4px).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const SPACING_SCALE: Record<string, number> = {
|
|
10
|
+
'px': 1, // Special 1px value
|
|
11
|
+
'0': 0,
|
|
12
|
+
'0.5': 2, // 0.125rem
|
|
13
|
+
'1': 4,
|
|
14
|
+
'1.5': 6,
|
|
15
|
+
'2': 8,
|
|
16
|
+
'2.5': 10,
|
|
17
|
+
'3': 12,
|
|
18
|
+
'3.5': 14,
|
|
19
|
+
'4': 16,
|
|
20
|
+
'5': 20,
|
|
21
|
+
'6': 24,
|
|
22
|
+
'7': 28,
|
|
23
|
+
'8': 32,
|
|
24
|
+
'9': 36,
|
|
25
|
+
'10': 40,
|
|
26
|
+
'11': 44,
|
|
27
|
+
'12': 48,
|
|
28
|
+
'14': 56,
|
|
29
|
+
'16': 64,
|
|
30
|
+
'20': 80,
|
|
31
|
+
'24': 96,
|
|
32
|
+
'28': 112,
|
|
33
|
+
'32': 128,
|
|
34
|
+
'36': 144,
|
|
35
|
+
'40': 160,
|
|
36
|
+
'44': 176,
|
|
37
|
+
'48': 192,
|
|
38
|
+
'52': 208,
|
|
39
|
+
'56': 224,
|
|
40
|
+
'60': 240,
|
|
41
|
+
'64': 256,
|
|
42
|
+
'72': 288,
|
|
43
|
+
'80': 320,
|
|
44
|
+
'96': 384,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a single spacing token value to its pixel size.
|
|
49
|
+
* Accepts:
|
|
50
|
+
* - Tailwind scale values: '4' → 16, '0.5' → 2, 'px' → 1
|
|
51
|
+
* - Arbitrary values: '[12px]', '[1.5rem]', '[1em]' (rem/em → px at 16px base)
|
|
52
|
+
* - Bare numbers not in the scale: parsed and multiplied by 4 (Tailwind default unit)
|
|
53
|
+
* - Unknown: 0
|
|
54
|
+
*/
|
|
55
|
+
export function resolveSpacing(value: string): number {
|
|
56
|
+
// Arbitrary value [Xpx] / [Xrem] / [Xem]
|
|
57
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
58
|
+
const inner = value.slice(1, -1);
|
|
59
|
+
const match = inner.match(/^(\d+(?:\.\d+)?)(px|rem|em)?$/);
|
|
60
|
+
if (match) {
|
|
61
|
+
let num = parseFloat(match[1]);
|
|
62
|
+
const unit = match[2] || 'px';
|
|
63
|
+
if (unit === 'rem' || unit === 'em') num *= 16;
|
|
64
|
+
return num;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (SPACING_SCALE[value] !== undefined) {
|
|
69
|
+
return SPACING_SCALE[value];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const num = parseFloat(value);
|
|
73
|
+
if (!isNaN(num)) {
|
|
74
|
+
return num * 4;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind spacing utilities → LayoutIR.
|
|
3
|
+
* Owns: gap-*, gap-x-*, gap-y-*, space-x-*, space-y-*, p-*, px-*, py-*,
|
|
4
|
+
* pt-*, pr-*, pb-*, pl-*.
|
|
5
|
+
*
|
|
6
|
+
* Each parser function reads a class list and mutates the IR in place.
|
|
7
|
+
* Behaviour is preserved 1:1 from the original parseGap / parsePadding
|
|
8
|
+
* methods on `LayoutParser`. Locked in by the spacing regression test.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { LayoutIR } from './ir';
|
|
12
|
+
import { resolveSpacing } from './spacing-scale';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse gap and space utilities into `ir.gap`, `ir.gapX`, `ir.gapY`.
|
|
16
|
+
* `space-x-*` / `space-y-*` map onto gap on the matching axis (Tailwind
|
|
17
|
+
* uses sibling margins; Figma auto-layout has no margin so we collapse
|
|
18
|
+
* the intent into the parent's gap).
|
|
19
|
+
*/
|
|
20
|
+
export function parseGap(classes: string[], ir: LayoutIR): void {
|
|
21
|
+
for (const cls of classes) {
|
|
22
|
+
// gap-* (both axes)
|
|
23
|
+
const gapMatch = cls.match(/^gap-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
24
|
+
if (gapMatch) {
|
|
25
|
+
const val = resolveSpacing(gapMatch[1]);
|
|
26
|
+
ir.gap = val;
|
|
27
|
+
ir.gapX = val;
|
|
28
|
+
ir.gapY = val;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// gap-x-*
|
|
33
|
+
const gapXMatch = cls.match(/^gap-x-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
34
|
+
if (gapXMatch) {
|
|
35
|
+
ir.gapX = resolveSpacing(gapXMatch[1]);
|
|
36
|
+
if (ir.layoutMode === 'HORIZONTAL') {
|
|
37
|
+
ir.gap = ir.gapX;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// gap-y-*
|
|
43
|
+
const gapYMatch = cls.match(/^gap-y-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
44
|
+
if (gapYMatch) {
|
|
45
|
+
ir.gapY = resolveSpacing(gapYMatch[1]);
|
|
46
|
+
if (ir.layoutMode === 'VERTICAL') {
|
|
47
|
+
ir.gap = ir.gapY;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// space-x-* → gap on horizontal layouts only. Negative form
|
|
53
|
+
// (`-space-x-2`) drives overlap (Avatar groups, badge clusters); maps to
|
|
54
|
+
// a negative item-spacing in Figma's auto-layout.
|
|
55
|
+
const spaceXMatch = cls.match(/^(-)?space-x-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
56
|
+
if (spaceXMatch && ir.layoutMode === 'HORIZONTAL') {
|
|
57
|
+
const sign = spaceXMatch[1] ? -1 : 1;
|
|
58
|
+
ir.gap = sign * resolveSpacing(spaceXMatch[2]);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// space-y-* → gap on vertical layouts only. Negative form mirrors -space-x-*.
|
|
63
|
+
const spaceYMatch = cls.match(/^(-)?space-y-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
64
|
+
if (spaceYMatch && ir.layoutMode === 'VERTICAL') {
|
|
65
|
+
const sign = spaceYMatch[1] ? -1 : 1;
|
|
66
|
+
ir.gap = sign * resolveSpacing(spaceYMatch[2]);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse padding utilities into `ir.paddingTop / Right / Bottom / Left`.
|
|
74
|
+
*/
|
|
75
|
+
export function parsePadding(classes: string[], ir: LayoutIR): void {
|
|
76
|
+
for (const cls of classes) {
|
|
77
|
+
const pMatch = cls.match(/^p-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
78
|
+
if (pMatch) {
|
|
79
|
+
const val = resolveSpacing(pMatch[1]);
|
|
80
|
+
ir.paddingTop = val;
|
|
81
|
+
ir.paddingRight = val;
|
|
82
|
+
ir.paddingBottom = val;
|
|
83
|
+
ir.paddingLeft = val;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pxMatch = cls.match(/^px-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
88
|
+
if (pxMatch) {
|
|
89
|
+
const val = resolveSpacing(pxMatch[1]);
|
|
90
|
+
ir.paddingLeft = val;
|
|
91
|
+
ir.paddingRight = val;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const pyMatch = cls.match(/^py-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
96
|
+
if (pyMatch) {
|
|
97
|
+
const val = resolveSpacing(pyMatch[1]);
|
|
98
|
+
ir.paddingTop = val;
|
|
99
|
+
ir.paddingBottom = val;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ptMatch = cls.match(/^pt-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
104
|
+
if (ptMatch) {
|
|
105
|
+
ir.paddingTop = resolveSpacing(ptMatch[1]);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const prMatch = cls.match(/^pr-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
109
|
+
if (prMatch) {
|
|
110
|
+
ir.paddingRight = resolveSpacing(prMatch[1]);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const pbMatch = cls.match(/^pb-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
114
|
+
if (pbMatch) {
|
|
115
|
+
ir.paddingBottom = resolveSpacing(pbMatch[1]);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const plMatch = cls.match(/^pl-(\d+(?:\.\d+)?|\[.+\])$/);
|
|
119
|
+
if (plMatch) {
|
|
120
|
+
ir.paddingLeft = resolveSpacing(plMatch[1]);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Orchestrator helper — calls parseGap and parsePadding in order.
|
|
128
|
+
* Provided for callers that want a single entrypoint for "all the
|
|
129
|
+
* spacing utilities in one go".
|
|
130
|
+
*/
|
|
131
|
+
export function parseSpacing(classes: string[], ir: LayoutIR): void {
|
|
132
|
+
parseGap(classes, ir);
|
|
133
|
+
parsePadding(classes, ir);
|
|
134
|
+
}
|