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
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
// --- Tailwind utilities: class parsing, style conversion, and codegen ---
|
|
2
2
|
|
|
3
|
-
import { COMPONENT_DEFS } from '
|
|
4
|
-
import { parseColor, nearestColorToken } from '
|
|
5
|
-
import type { RGB } from '
|
|
6
|
-
import { getComponentDef } from '
|
|
7
|
-
import { bindColorVariable } from '
|
|
3
|
+
import { COMPONENT_DEFS } from '../tokens';
|
|
4
|
+
import { parseColor, nearestColorToken } from '../tokens';
|
|
5
|
+
import type { RGB } from '../tokens';
|
|
6
|
+
import { getComponentDef } from '../components';
|
|
7
|
+
import { bindColorVariable } from '../tokens';
|
|
8
8
|
import {
|
|
9
9
|
parseUtilityClass,
|
|
10
|
-
hasResponsiveVariant as hasResponsiveVariantSemantic,
|
|
11
|
-
variantState as variantStateSemantic,
|
|
12
10
|
spacingValue,
|
|
13
11
|
resolveMaxWidth,
|
|
14
12
|
resolveRadius,
|
|
15
13
|
parseLength as parseLengthSemantic,
|
|
16
14
|
} from './utility-resolver';
|
|
17
|
-
import { parseRadialAnchorFromUtility, radialGradientTransformFromAnchor, type RadialAnchor } from '
|
|
15
|
+
import { parseRadialAnchorFromUtility, radialGradientTransformFromAnchor, type RadialAnchor } from '../effects';
|
|
18
16
|
import { rotationTransformAroundPointRadians } from './transform-math';
|
|
17
|
+
// Internal mark functions used during CSS processing — resolved post-append by deferred-layout.ts
|
|
18
|
+
import {
|
|
19
|
+
markAbsoluteNode,
|
|
20
|
+
markAspectRatio,
|
|
21
|
+
markPositionInfo,
|
|
22
|
+
markFullWidthNode,
|
|
23
|
+
markFullHeightNode,
|
|
24
|
+
markFixedHeightNode,
|
|
25
|
+
markFixedWidthNode,
|
|
26
|
+
markSelfAlignmentNode,
|
|
27
|
+
markFlexBasisNode,
|
|
28
|
+
markMinWidthNode,
|
|
29
|
+
markMaxWidthNode,
|
|
30
|
+
markGridColumnsNode,
|
|
31
|
+
markColSpanNode,
|
|
32
|
+
markFractionWidthNode,
|
|
33
|
+
markCssGridVerticalFrame,
|
|
34
|
+
parseFractionToken,
|
|
35
|
+
setFrameCrossAlign,
|
|
36
|
+
setFrameInlineAlign,
|
|
37
|
+
shouldApplyAtom,
|
|
38
|
+
BORDER_WIDTH_CLASSES,
|
|
39
|
+
DEFERRED_TOP_RELATIVE_NODES,
|
|
40
|
+
getRingInfoFromClasses,
|
|
41
|
+
markRingNode,
|
|
42
|
+
} from '../layout';
|
|
19
43
|
|
|
20
44
|
// ---------------------------------------------------------------------------
|
|
21
45
|
// Types
|
|
@@ -72,583 +96,15 @@ const FALLBACK_COLOR_TOKENS: Record<string, string> = {
|
|
|
72
96
|
type StyleMapEntry = { declarations: Record<string, string>; media?: string };
|
|
73
97
|
type StyleMap = Record<string, StyleMapEntry[]>;
|
|
74
98
|
|
|
75
|
-
const ABSOLUTE_NODES = new WeakSet<SceneNode>();
|
|
76
|
-
const CSS_GRID_VERTICAL_FRAMES = new WeakSet<SceneNode>(); // grid without grid-cols-N → single-column, children stretch
|
|
77
|
-
const FULL_WIDTH_NODES = new WeakSet<SceneNode>();
|
|
78
|
-
const FULL_HEIGHT_NODES = new WeakSet<SceneNode>();
|
|
79
|
-
const FRACTION_WIDTH_NODES = new WeakMap<SceneNode, number>();
|
|
80
|
-
const FIXED_WIDTH_NODES = new WeakSet<SceneNode>();
|
|
81
|
-
const SELF_ALIGNMENT_NODES = new WeakMap<SceneNode, 'MIN' | 'CENTER' | 'MAX' | 'STRETCH'>();
|
|
82
|
-
const FLEX_BASIS_NODES = new WeakMap<SceneNode, number>();
|
|
83
|
-
const MIN_WIDTH_NODES = new WeakMap<SceneNode, number>();
|
|
84
|
-
const GRID_COLUMNS_NODES = new WeakMap<SceneNode, number>();
|
|
85
|
-
const COL_SPAN_NODES = new WeakMap<SceneNode, number>(); // child → number of columns spanned
|
|
86
|
-
const POSITION_INFO_NODES = new WeakMap<SceneNode, { top?: number; bottom?: number; left?: number; right?: number; hintParentHeight?: number; inset?: number }>();
|
|
87
|
-
const DEFERRED_BOTTOM_NODES = new WeakMap<SceneNode, number>(); // child → bottom pixel offset
|
|
88
|
-
const DEFERRED_TOP_RELATIVE_NODES = new WeakMap<SceneNode, number>(); // child → (parentHeight - N) top offset
|
|
89
|
-
const DEFERRED_CENTER_Y_NODES = new WeakSet<SceneNode>(); // absolute children needing cross-axis centering
|
|
90
|
-
const BORDER_WIDTH_CLASSES = new WeakMap<SceneNode, string[]>();
|
|
91
99
|
|
|
92
100
|
function getStyleMap(): StyleMap | null {
|
|
93
|
-
const defs
|
|
101
|
+
const defs = COMPONENT_DEFS;
|
|
94
102
|
return defs && defs.styleMap ? (defs.styleMap as StyleMap) : null;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
function getPaletteTokens(): Record<string, string> {
|
|
98
|
-
const defs
|
|
99
|
-
return defs && defs.paletteTokens ?
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function markAbsoluteNode(node: SceneNode): void {
|
|
103
|
-
ABSOLUTE_NODES.add(node);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function markPositionInfo(node: SceneNode, info: { top?: number; bottom?: number; left?: number; right?: number; hintParentHeight?: number; inset?: number }): void {
|
|
107
|
-
const existing = POSITION_INFO_NODES.get(node) || {};
|
|
108
|
-
POSITION_INFO_NODES.set(node, { ...existing, ...info });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function applyAbsoluteIfPossible(child: SceneNode, parent: FrameNode): void {
|
|
112
|
-
if (!ABSOLUTE_NODES.has(child)) return;
|
|
113
|
-
if ('layoutMode' in parent && parent.layoutMode && parent.layoutMode !== 'NONE') {
|
|
114
|
-
try {
|
|
115
|
-
(child as any).layoutPositioning = 'ABSOLUTE';
|
|
116
|
-
|
|
117
|
-
// Apply positioning from stored position info
|
|
118
|
-
const posInfo = POSITION_INFO_NODES.get(child);
|
|
119
|
-
if (posInfo) {
|
|
120
|
-
// If the child hints a minimum parent height (e.g. gradient-blob inside collapsed container)
|
|
121
|
-
if (posInfo.hintParentHeight != null && 'resize' in parent) {
|
|
122
|
-
try {
|
|
123
|
-
const ph = posInfo.hintParentHeight;
|
|
124
|
-
const pw = Math.max(1, parent.width);
|
|
125
|
-
// Respect explicit fixed-height parents. Only auto-expand when the
|
|
126
|
-
// parent is effectively unconstrained/collapsed.
|
|
127
|
-
let heightIsFixed = false;
|
|
128
|
-
if ('layoutMode' in parent && 'primaryAxisSizingMode' in parent && 'counterAxisSizingMode' in parent) {
|
|
129
|
-
if ((parent as any).layoutMode === 'VERTICAL') {
|
|
130
|
-
heightIsFixed = (parent as any).primaryAxisSizingMode === 'FIXED';
|
|
131
|
-
} else if ((parent as any).layoutMode === 'HORIZONTAL') {
|
|
132
|
-
heightIsFixed = (parent as any).counterAxisSizingMode === 'FIXED';
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const parentCollapsed = parent.height <= 1;
|
|
136
|
-
if (!heightIsFixed && parentCollapsed && parent.height < ph) {
|
|
137
|
-
parent.resize(pw, ph);
|
|
138
|
-
if ('primaryAxisSizingMode' in parent) (parent as any).primaryAxisSizingMode = 'FIXED';
|
|
139
|
-
}
|
|
140
|
-
} catch (_e) { /* ignore */ }
|
|
141
|
-
}
|
|
142
|
-
if ('x' in child && 'y' in child) {
|
|
143
|
-
const childFrame = child as FrameNode;
|
|
144
|
-
// inset-{n}: position at (n, n) and resize to fill parent minus inset on all sides
|
|
145
|
-
if (posInfo.inset != null) {
|
|
146
|
-
const inset = posInfo.inset;
|
|
147
|
-
childFrame.x = inset;
|
|
148
|
-
childFrame.y = inset;
|
|
149
|
-
const targetW = Math.max(1, parent.width - inset * 2);
|
|
150
|
-
const targetH = Math.max(1, parent.height - inset * 2);
|
|
151
|
-
try {
|
|
152
|
-
(childFrame as any).resize(targetW, targetH);
|
|
153
|
-
if ('primaryAxisSizingMode' in childFrame) (childFrame as any).primaryAxisSizingMode = 'FIXED';
|
|
154
|
-
if ('counterAxisSizingMode' in childFrame) (childFrame as any).counterAxisSizingMode = 'FIXED';
|
|
155
|
-
} catch (_e) { /* ignore */ }
|
|
156
|
-
} else {
|
|
157
|
-
if (posInfo.left != null) {
|
|
158
|
-
childFrame.x = posInfo.left;
|
|
159
|
-
} else if (posInfo.right != null) {
|
|
160
|
-
childFrame.x = parent.width - childFrame.width - posInfo.right;
|
|
161
|
-
}
|
|
162
|
-
if (posInfo.top != null) {
|
|
163
|
-
childFrame.y = posInfo.top;
|
|
164
|
-
} else if (posInfo.bottom != null) {
|
|
165
|
-
// Defer: parent height may not be final yet (flow children added after this call)
|
|
166
|
-
DEFERRED_BOTTOM_NODES.set(child, posInfo.bottom);
|
|
167
|
-
} else {
|
|
168
|
-
// No explicit top/bottom: CSS places absolute children at the cross-axis
|
|
169
|
-
// static position. For items-center parents this means vertically centered.
|
|
170
|
-
if ((parent as any).counterAxisAlignItems === 'CENTER') {
|
|
171
|
-
DEFERRED_CENTER_Y_NODES.add(child);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} catch (_err) {
|
|
178
|
-
// ignore
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
ABSOLUTE_NODES.delete(child);
|
|
182
|
-
POSITION_INFO_NODES.delete(child);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Call after all children have been appended to a frame to apply bottom-anchored positions. */
|
|
186
|
-
export function applyDeferredBottomPositioning(parent: FrameNode): void {
|
|
187
|
-
for (const child of (parent.children as SceneNode[])) {
|
|
188
|
-
const bottom = DEFERRED_BOTTOM_NODES.get(child);
|
|
189
|
-
if (bottom != null) {
|
|
190
|
-
try {
|
|
191
|
-
(child as any).y = parent.height - (child as any).height - bottom;
|
|
192
|
-
} catch (_e) { /* ignore */ }
|
|
193
|
-
DEFERRED_BOTTOM_NODES.delete(child);
|
|
194
|
-
}
|
|
195
|
-
// Handle top-[calc(100%-Nrem)]: top = parentHeight - N
|
|
196
|
-
const topRelative = DEFERRED_TOP_RELATIVE_NODES.get(child);
|
|
197
|
-
if (topRelative != null) {
|
|
198
|
-
try {
|
|
199
|
-
(child as any).y = parent.height - topRelative;
|
|
200
|
-
} catch (_e) { /* ignore */ }
|
|
201
|
-
DEFERRED_TOP_RELATIVE_NODES.delete(child);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Call after all children have been appended to center absolute children with no explicit top/bottom. */
|
|
207
|
-
export function applyDeferredCenterYPositioning(parent: FrameNode): void {
|
|
208
|
-
const parentHeight = parent.height;
|
|
209
|
-
if (parentHeight <= 0) return;
|
|
210
|
-
for (const child of (parent.children as SceneNode[])) {
|
|
211
|
-
if (DEFERRED_CENTER_Y_NODES.has(child)) {
|
|
212
|
-
try {
|
|
213
|
-
const childHeight = (child as any).height || 0;
|
|
214
|
-
(child as any).y = Math.round((parentHeight - childHeight) / 2);
|
|
215
|
-
} catch (_e) { /* ignore */ }
|
|
216
|
-
DEFERRED_CENTER_Y_NODES.delete(child);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function markFullWidthNode(node: SceneNode): void {
|
|
222
|
-
FULL_WIDTH_NODES.add(node);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function markFullHeightNode(node: SceneNode): void {
|
|
226
|
-
FULL_HEIGHT_NODES.add(node);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function markFixedWidthNode(node: SceneNode): void {
|
|
230
|
-
FIXED_WIDTH_NODES.add(node);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function markSelfAlignmentNode(node: SceneNode, align: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH'): void {
|
|
234
|
-
SELF_ALIGNMENT_NODES.set(node, align);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function markFlexBasisNode(node: SceneNode, basis: number): void {
|
|
238
|
-
if (!Number.isFinite(basis)) return;
|
|
239
|
-
FLEX_BASIS_NODES.set(node, basis);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function markMinWidthNode(node: SceneNode, minWidth: number): void {
|
|
243
|
-
if (!Number.isFinite(minWidth)) return;
|
|
244
|
-
MIN_WIDTH_NODES.set(node, minWidth);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function markGridColumnsNode(node: SceneNode, cols: number): void {
|
|
248
|
-
if (!Number.isFinite(cols) || cols <= 0) return;
|
|
249
|
-
GRID_COLUMNS_NODES.set(node, cols);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function markColSpanNode(node: SceneNode, span: number): void {
|
|
253
|
-
if (!Number.isFinite(span) || span <= 0) return;
|
|
254
|
-
COL_SPAN_NODES.set(node, span);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function getColSpanNode(node: SceneNode): number {
|
|
258
|
-
return COL_SPAN_NODES.get(node) ?? 1;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function markFractionWidthNode(node: SceneNode, fraction: number): void {
|
|
262
|
-
if (!Number.isFinite(fraction) || fraction <= 0) return;
|
|
263
|
-
FRACTION_WIDTH_NODES.set(node, fraction);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function parseFractionToken(token: string): number | null {
|
|
267
|
-
if (!token.includes('/')) return null;
|
|
268
|
-
const [rawNum, rawDen] = token.split('/');
|
|
269
|
-
const num = Number(rawNum);
|
|
270
|
-
const den = Number(rawDen);
|
|
271
|
-
if (!Number.isFinite(num) || !Number.isFinite(den) || den === 0) return null;
|
|
272
|
-
return num / den;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function reapplyDirectionalBordersIfNeeded(node: SceneNode): void {
|
|
276
|
-
const classes = BORDER_WIDTH_CLASSES.get(node);
|
|
277
|
-
if (!classes || classes.length === 0) return;
|
|
278
|
-
if (!('strokes' in node)) return;
|
|
279
|
-
applyBorderWidthUtilities(node as FrameNode, classes);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function applyFullWidthIfPossible(
|
|
283
|
-
child: SceneNode,
|
|
284
|
-
parent: FrameNode,
|
|
285
|
-
options?: { skipFullWidth?: boolean; widthOverride?: number }
|
|
286
|
-
): void {
|
|
287
|
-
const align = SELF_ALIGNMENT_NODES.get(child);
|
|
288
|
-
const hasSelfAlign = align != null;
|
|
289
|
-
if (align) {
|
|
290
|
-
if (align === 'STRETCH') {
|
|
291
|
-
if ('layoutAlign' in child) {
|
|
292
|
-
try { (child as any).layoutAlign = align; } catch (_err) { /* ignore */ }
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
// MIN / CENTER / MAX: layoutAlign is deprecated for these; use layoutSizingHorizontal = 'HUG' as the best proxy
|
|
296
|
-
try { (child as any).layoutSizingHorizontal = 'HUG'; } catch (_err) { /* ignore if no auto-layout */ }
|
|
297
|
-
}
|
|
298
|
-
SELF_ALIGNMENT_NODES.delete(child);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const basis = FLEX_BASIS_NODES.get(child);
|
|
302
|
-
if (basis != null && basis > 0 && 'resize' in child) {
|
|
303
|
-
try {
|
|
304
|
-
if (parent.layoutMode === 'HORIZONTAL') {
|
|
305
|
-
(child as any).resize(basis, (child as any).height);
|
|
306
|
-
if ('primaryAxisSizingMode' in child) (child as any).primaryAxisSizingMode = 'FIXED';
|
|
307
|
-
} else if (parent.layoutMode === 'VERTICAL') {
|
|
308
|
-
(child as any).resize((child as any).width, basis);
|
|
309
|
-
if ('primaryAxisSizingMode' in child) (child as any).primaryAxisSizingMode = 'FIXED';
|
|
310
|
-
}
|
|
311
|
-
} catch (_err) {
|
|
312
|
-
// ignore
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (basis != null) {
|
|
316
|
-
FLEX_BASIS_NODES.delete(child);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const minWidth = MIN_WIDTH_NODES.get(child);
|
|
320
|
-
if (minWidth != null && 'resize' in child) {
|
|
321
|
-
const currentWidth = (child as any).width as number;
|
|
322
|
-
if (!Number.isFinite(currentWidth) || currentWidth < minWidth) {
|
|
323
|
-
try {
|
|
324
|
-
(child as any).resize(minWidth, (child as any).height);
|
|
325
|
-
if (parent.layoutMode === 'HORIZONTAL' && 'primaryAxisSizingMode' in child) {
|
|
326
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
327
|
-
} else if (parent.layoutMode === 'VERTICAL' && 'counterAxisSizingMode' in child) {
|
|
328
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
329
|
-
}
|
|
330
|
-
} catch (_err) {
|
|
331
|
-
// ignore
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
MIN_WIDTH_NODES.delete(child);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const skipFullWidth = !!(options && options.skipFullWidth);
|
|
338
|
-
const widthOverride = options && typeof options.widthOverride === 'number' && Number.isFinite(options.widthOverride)
|
|
339
|
-
? options.widthOverride
|
|
340
|
-
: null;
|
|
341
|
-
const widthBase = widthOverride != null ? widthOverride : parent.width;
|
|
342
|
-
// CSS grid (single-column) children stretch to fill the column by default (align-items: stretch).
|
|
343
|
-
// Don't stretch absolute-positioned children — they're handled separately.
|
|
344
|
-
const isGridStretchChild = !skipFullWidth && CSS_GRID_VERTICAL_FRAMES.has(parent) && !ABSOLUTE_NODES.has(child);
|
|
345
|
-
const hasFullWidth = skipFullWidth ? false : (FULL_WIDTH_NODES.has(child) || isGridStretchChild);
|
|
346
|
-
const fractionWidth = skipFullWidth ? null : FRACTION_WIDTH_NODES.get(child);
|
|
347
|
-
const hasFixedWidth = FIXED_WIDTH_NODES.has(child);
|
|
348
|
-
const hasFullHeight = FULL_HEIGHT_NODES.has(child);
|
|
349
|
-
if (!hasFullWidth && fractionWidth == null && !hasFullHeight && !hasFixedWidth) return;
|
|
350
|
-
if (hasFixedWidth && parent.layoutMode === 'VERTICAL' && !hasSelfAlign && 'layoutAlign' in child) {
|
|
351
|
-
try {
|
|
352
|
-
(child as any).layoutAlign = 'INHERIT';
|
|
353
|
-
} catch (_err) {
|
|
354
|
-
// ignore
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
try {
|
|
358
|
-
if (!hasFullWidth && fractionWidth != null && 'resize' in child && widthBase > 0) {
|
|
359
|
-
// widthOverride is already content-width; only subtract padding when using parent.width
|
|
360
|
-
const padding = widthOverride != null ? 0 : (parent.paddingLeft || 0) + (parent.paddingRight || 0);
|
|
361
|
-
const targetWidth = Math.max(0, widthBase - padding) * fractionWidth;
|
|
362
|
-
try {
|
|
363
|
-
(child as any).resize(targetWidth, (child as any).height);
|
|
364
|
-
// Prevent fractional width children from expanding
|
|
365
|
-
if ('layoutGrow' in child) {
|
|
366
|
-
(child as any).layoutGrow = 0;
|
|
367
|
-
}
|
|
368
|
-
if (parent.layoutMode === 'VERTICAL' && !hasSelfAlign && 'layoutAlign' in child) {
|
|
369
|
-
(child as any).layoutAlign = 'INHERIT';
|
|
370
|
-
}
|
|
371
|
-
// Set sizing mode to FIXED so the width is respected
|
|
372
|
-
if ('layoutMode' in child) {
|
|
373
|
-
const childLayout = (child as any).layoutMode;
|
|
374
|
-
if (childLayout === 'HORIZONTAL' && 'primaryAxisSizingMode' in child) {
|
|
375
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
376
|
-
} else if (childLayout === 'VERTICAL' && 'counterAxisSizingMode' in child) {
|
|
377
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
378
|
-
}
|
|
379
|
-
} else if (parent.layoutMode === 'VERTICAL' && 'counterAxisSizingMode' in child) {
|
|
380
|
-
// For non-layout children in vertical parent, fix the width
|
|
381
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
382
|
-
}
|
|
383
|
-
} catch (_err) {
|
|
384
|
-
// ignore resize errors
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (hasFullWidth) {
|
|
389
|
-
if (parent.layoutMode === 'HORIZONTAL') {
|
|
390
|
-
// When parent uses SPACE_BETWEEN, don't apply layoutGrow=1 because that would
|
|
391
|
-
// make this child consume all space, leaving nothing for siblings.
|
|
392
|
-
// In CSS flexbox, width:100% with justify-content:space-between still spaces items at edges.
|
|
393
|
-
// In Figma, we need to explicitly set layoutGrow=0 and let SPACE_BETWEEN distribute items.
|
|
394
|
-
const isSpaceBetween = (parent as any).primaryAxisAlignItems === 'SPACE_BETWEEN';
|
|
395
|
-
if (isSpaceBetween) {
|
|
396
|
-
// Explicitly set to 0 to prevent any expansion
|
|
397
|
-
(child as any).layoutGrow = 0;
|
|
398
|
-
// Also ensure the child sizes to its content, not to fill
|
|
399
|
-
if ('primaryAxisSizingMode' in child) {
|
|
400
|
-
(child as any).primaryAxisSizingMode = 'AUTO';
|
|
401
|
-
}
|
|
402
|
-
} else {
|
|
403
|
-
(child as any).layoutGrow = 1;
|
|
404
|
-
}
|
|
405
|
-
if (
|
|
406
|
-
(('primaryAxisSizingMode' in parent && (parent as any).primaryAxisSizingMode === 'FIXED') || widthOverride != null)
|
|
407
|
-
&& 'resize' in child
|
|
408
|
-
&& !isSpaceBetween
|
|
409
|
-
&& widthBase > 0
|
|
410
|
-
) {
|
|
411
|
-
try {
|
|
412
|
-
(child as any).resize(widthBase, (child as any).height);
|
|
413
|
-
if ('primaryAxisSizingMode' in child) {
|
|
414
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
415
|
-
}
|
|
416
|
-
} catch (_err) {
|
|
417
|
-
// ignore resize errors
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
} else if (parent.layoutMode === 'VERTICAL') {
|
|
421
|
-
// If the child is HORIZONTAL with SPACE_BETWEEN and already has FIXED sizing,
|
|
422
|
-
// don't use STRETCH as it conflicts. Instead, keep the explicit FIXED width.
|
|
423
|
-
const childIsHorizontal = 'layoutMode' in child && (child as any).layoutMode === 'HORIZONTAL';
|
|
424
|
-
const childHasSpaceBetween = childIsHorizontal && (child as any).primaryAxisAlignItems === 'SPACE_BETWEEN';
|
|
425
|
-
const childHasFixedWidth = 'primaryAxisSizingMode' in child && (child as any).primaryAxisSizingMode === 'FIXED';
|
|
426
|
-
if (childHasSpaceBetween && childHasFixedWidth) {
|
|
427
|
-
// Keep the FIXED sizing, but set layoutAlign to FILL to take parent width
|
|
428
|
-
// Actually, for SPACE_BETWEEN to work, we need the child to have a specific width
|
|
429
|
-
// So we resize it to parent width and keep FIXED
|
|
430
|
-
if ('resize' in child && widthBase > 0) {
|
|
431
|
-
try {
|
|
432
|
-
const padding = (parent.paddingLeft || 0) + (parent.paddingRight || 0);
|
|
433
|
-
(child as any).resize(widthBase - padding, (child as any).height);
|
|
434
|
-
} catch (_err) {
|
|
435
|
-
// ignore
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
} else {
|
|
439
|
-
(child as any).layoutAlign = 'STRETCH';
|
|
440
|
-
if (
|
|
441
|
-
(('counterAxisSizingMode' in parent && (parent as any).counterAxisSizingMode === 'FIXED') || widthOverride != null)
|
|
442
|
-
&& 'resize' in child
|
|
443
|
-
) {
|
|
444
|
-
try {
|
|
445
|
-
// widthOverride is already content-width (padding already subtracted by caller).
|
|
446
|
-
// When falling back to parent.width we must subtract padding ourselves.
|
|
447
|
-
const padding = widthOverride != null ? 0 : (parent.paddingLeft || 0) + (parent.paddingRight || 0);
|
|
448
|
-
const targetWidth = Math.max(0, widthBase - padding);
|
|
449
|
-
if (targetWidth > 0) {
|
|
450
|
-
(child as any).resize(targetWidth, (child as any).height);
|
|
451
|
-
if ('layoutMode' in child) {
|
|
452
|
-
const childLayout = (child as any).layoutMode;
|
|
453
|
-
if (childLayout === 'HORIZONTAL' && 'primaryAxisSizingMode' in child) {
|
|
454
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
455
|
-
} else if (childLayout === 'VERTICAL' && 'counterAxisSizingMode' in child) {
|
|
456
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
457
|
-
}
|
|
458
|
-
} else if ('counterAxisSizingMode' in child) {
|
|
459
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
} catch (_err) {
|
|
463
|
-
// ignore
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
if (hasFullHeight) {
|
|
470
|
-
if (parent.layoutMode === 'VERTICAL') {
|
|
471
|
-
if ('layoutGrow' in child && (parent as any).primaryAxisSizingMode === 'FIXED') {
|
|
472
|
-
(child as any).layoutGrow = 1;
|
|
473
|
-
}
|
|
474
|
-
} else if (parent.layoutMode === 'HORIZONTAL') {
|
|
475
|
-
if ('layoutAlign' in child) {
|
|
476
|
-
(child as any).layoutAlign = 'STRETCH';
|
|
477
|
-
}
|
|
478
|
-
if (
|
|
479
|
-
'counterAxisSizingMode' in parent
|
|
480
|
-
&& (parent as any).counterAxisSizingMode === 'FIXED'
|
|
481
|
-
&& 'resize' in child
|
|
482
|
-
) {
|
|
483
|
-
try {
|
|
484
|
-
const padding = (parent.paddingTop || 0) + (parent.paddingBottom || 0);
|
|
485
|
-
const targetHeight = Math.max(0, parent.height - padding);
|
|
486
|
-
if (targetHeight > 0) {
|
|
487
|
-
(child as any).resize((child as any).width, targetHeight);
|
|
488
|
-
if ('counterAxisSizingMode' in child) {
|
|
489
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
} catch (_err) {
|
|
493
|
-
// ignore
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
} catch (_err) {
|
|
499
|
-
// ignore
|
|
500
|
-
}
|
|
501
|
-
reapplyDirectionalBordersIfNeeded(child);
|
|
502
|
-
// Keep mark so we can re-apply after parent resize.
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function applyGridColumnsIfPossible(
|
|
506
|
-
frame: FrameNode,
|
|
507
|
-
widthOverride?: number,
|
|
508
|
-
colsOverride?: number
|
|
509
|
-
): void {
|
|
510
|
-
const cols = colsOverride != null ? colsOverride : GRID_COLUMNS_NODES.get(frame);
|
|
511
|
-
if (!cols || cols <= 0) return;
|
|
512
|
-
if (colsOverride != null) {
|
|
513
|
-
GRID_COLUMNS_NODES.set(frame, cols);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
try {
|
|
517
|
-
if (frame.layoutMode !== 'HORIZONTAL') {
|
|
518
|
-
frame.layoutMode = 'HORIZONTAL';
|
|
519
|
-
}
|
|
520
|
-
if ((frame as any).layoutWrap !== undefined) {
|
|
521
|
-
(frame as any).layoutWrap = 'WRAP';
|
|
522
|
-
}
|
|
523
|
-
} catch (_err) {
|
|
524
|
-
// ignore
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const totalWidth = widthOverride && Number.isFinite(widthOverride) ? widthOverride : frame.width;
|
|
528
|
-
if (!totalWidth || totalWidth <= 0) return;
|
|
529
|
-
|
|
530
|
-
const gap = frame.itemSpacing || 0;
|
|
531
|
-
const padding = (frame.paddingLeft || 0) + (frame.paddingRight || 0);
|
|
532
|
-
const available = totalWidth - padding - gap * (cols - 1);
|
|
533
|
-
if (available <= 0) return;
|
|
534
|
-
|
|
535
|
-
const childWidth = Math.max(0, available / cols);
|
|
536
|
-
if ((frame as any).counterAxisSpacing !== undefined) {
|
|
537
|
-
(frame as any).counterAxisSpacing = gap;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
for (const child of frame.children) {
|
|
541
|
-
if (!('resize' in child)) continue;
|
|
542
|
-
try {
|
|
543
|
-
if ((child as any).layoutPositioning === 'ABSOLUTE') continue;
|
|
544
|
-
const span = Math.min(COL_SPAN_NODES.get(child) ?? 1, cols);
|
|
545
|
-
const spanWidth = childWidth * span + gap * (span - 1);
|
|
546
|
-
// For text nodes, fix the width so textAlignHorizontal (e.g. text-right) takes effect.
|
|
547
|
-
// Without this, WIDTH_AND_HEIGHT auto-resize snaps the node back to text-fit width.
|
|
548
|
-
if ((child as any).textAutoResize !== undefined) {
|
|
549
|
-
(child as any).textAutoResize = 'HEIGHT';
|
|
550
|
-
}
|
|
551
|
-
(child as any).resize(spanWidth, (child as any).height);
|
|
552
|
-
// Prevent grid children from expanding beyond calculated width
|
|
553
|
-
if ('layoutGrow' in child) {
|
|
554
|
-
(child as any).layoutGrow = 0;
|
|
555
|
-
}
|
|
556
|
-
if ('layoutMode' in child) {
|
|
557
|
-
const childLayout = (child as any).layoutMode;
|
|
558
|
-
if (childLayout === 'VERTICAL' && 'counterAxisSizingMode' in child) {
|
|
559
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
560
|
-
} else if (childLayout === 'HORIZONTAL' && 'primaryAxisSizingMode' in child) {
|
|
561
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
562
|
-
}
|
|
563
|
-
} else if ('primaryAxisSizingMode' in child) {
|
|
564
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
565
|
-
}
|
|
566
|
-
reapplyDirectionalBordersIfNeeded(child);
|
|
567
|
-
} catch (_err) {
|
|
568
|
-
// ignore
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
export function hasGridColumnsNode(node: SceneNode): boolean {
|
|
574
|
-
return GRID_COLUMNS_NODES.has(node);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
export function getGridColumnsNode(node: SceneNode): number | null {
|
|
578
|
-
const cols = GRID_COLUMNS_NODES.get(node);
|
|
579
|
-
return cols != null ? cols : null;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
export function applyFlexGrowIfPossible(frame: FrameNode, widthOverride?: number): void {
|
|
583
|
-
if (frame.layoutMode !== 'HORIZONTAL') return;
|
|
584
|
-
|
|
585
|
-
const padding = (frame.paddingLeft || 0) + (frame.paddingRight || 0);
|
|
586
|
-
const gap = frame.itemSpacing || 0;
|
|
587
|
-
const children = frame.children.filter(child => {
|
|
588
|
-
const positioning = (child as any).layoutPositioning;
|
|
589
|
-
return positioning !== 'ABSOLUTE';
|
|
590
|
-
});
|
|
591
|
-
if (children.length === 0) return;
|
|
592
|
-
|
|
593
|
-
let fixedWidth = 0;
|
|
594
|
-
let growTotal = 0;
|
|
595
|
-
const growChildren: SceneNode[] = [];
|
|
596
|
-
|
|
597
|
-
for (const child of children) {
|
|
598
|
-
const grow = (child as any).layoutGrow;
|
|
599
|
-
if (Number.isFinite(grow) && grow > 0) {
|
|
600
|
-
growTotal += grow;
|
|
601
|
-
growChildren.push(child);
|
|
602
|
-
} else {
|
|
603
|
-
fixedWidth += (child as any).width || 0;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (growChildren.length === 0 || growTotal <= 0) return;
|
|
608
|
-
|
|
609
|
-
// Determine target width: use override, frame width, or compute based on children
|
|
610
|
-
let targetWidth = widthOverride && Number.isFinite(widthOverride) ? widthOverride : frame.width;
|
|
611
|
-
|
|
612
|
-
// If no width available, compute a minimum width based on fixed children + space for grow children
|
|
613
|
-
const totalGap = gap * Math.max(0, children.length - 1);
|
|
614
|
-
if (!targetWidth || targetWidth <= 0) {
|
|
615
|
-
// Use 150px per grow unit as a reasonable default for flex-grow children
|
|
616
|
-
const growWidth = 150 * growTotal;
|
|
617
|
-
targetWidth = padding + totalGap + fixedWidth + growWidth;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Resize frame to target width and set to FIXED
|
|
621
|
-
try {
|
|
622
|
-
frame.resize(targetWidth, frame.height);
|
|
623
|
-
frame.primaryAxisSizingMode = 'FIXED';
|
|
624
|
-
} catch (_err) {
|
|
625
|
-
// ignore
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const remaining = targetWidth - padding - totalGap - fixedWidth;
|
|
629
|
-
if (!Number.isFinite(remaining) || remaining <= 0) return;
|
|
630
|
-
|
|
631
|
-
for (const child of growChildren) {
|
|
632
|
-
const grow = (child as any).layoutGrow;
|
|
633
|
-
const width = remaining * (grow / growTotal);
|
|
634
|
-
if (!Number.isFinite(width) || width <= 0) continue;
|
|
635
|
-
if (!('resize' in child)) continue;
|
|
636
|
-
try {
|
|
637
|
-
(child as any).resize(width, (child as any).height);
|
|
638
|
-
if ('layoutMode' in child) {
|
|
639
|
-
const childLayout = (child as any).layoutMode;
|
|
640
|
-
if (childLayout === 'VERTICAL' && 'counterAxisSizingMode' in child) {
|
|
641
|
-
(child as any).counterAxisSizingMode = 'FIXED';
|
|
642
|
-
} else if (childLayout === 'HORIZONTAL' && 'primaryAxisSizingMode' in child) {
|
|
643
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
644
|
-
}
|
|
645
|
-
} else if ('primaryAxisSizingMode' in child) {
|
|
646
|
-
(child as any).primaryAxisSizingMode = 'FIXED';
|
|
647
|
-
}
|
|
648
|
-
} catch (_err) {
|
|
649
|
-
// ignore
|
|
650
|
-
}
|
|
651
|
-
}
|
|
106
|
+
const defs = COMPONENT_DEFS;
|
|
107
|
+
return defs && defs.paletteTokens ? defs.paletteTokens : {};
|
|
652
108
|
}
|
|
653
109
|
|
|
654
110
|
function cssVarToToken(value: string): string | null {
|
|
@@ -667,27 +123,6 @@ function resolveSpacingToken(token: string, spacingScale: Record<string, number>
|
|
|
667
123
|
return spacingValue(token, spacingScale);
|
|
668
124
|
}
|
|
669
125
|
|
|
670
|
-
const STATE_VARIANTS = new Set([
|
|
671
|
-
'hover',
|
|
672
|
-
'focus',
|
|
673
|
-
'focus-visible',
|
|
674
|
-
'disabled',
|
|
675
|
-
'active',
|
|
676
|
-
'aria-invalid',
|
|
677
|
-
]);
|
|
678
|
-
|
|
679
|
-
function isStateVariant(variant: string): boolean {
|
|
680
|
-
if (STATE_VARIANTS.has(variant)) return true;
|
|
681
|
-
return variant.startsWith('data-[state=');
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function shouldApplyAtom(atom: ReturnType<typeof parseUtilityClass>, state: string): boolean {
|
|
685
|
-
if (hasResponsiveVariantSemantic(atom.variants)) return false;
|
|
686
|
-
if (state === 'default') return atom.variants.length === 0;
|
|
687
|
-
if (!atom.variants.length) return false;
|
|
688
|
-
if (!atom.variants.every(isStateVariant)) return false;
|
|
689
|
-
return variantStateSemantic(atom.variants) === state;
|
|
690
|
-
}
|
|
691
126
|
|
|
692
127
|
function parseOpacityToken(token: string): number | null {
|
|
693
128
|
if (!token) return null;
|
|
@@ -770,35 +205,26 @@ function parseFlexShorthand(value: string): { grow: number | null; basis: number
|
|
|
770
205
|
}
|
|
771
206
|
|
|
772
207
|
function getBorderSideWeights(frame: FrameNode): { top: number; right: number; bottom: number; left: number } {
|
|
773
|
-
const target = frame as any;
|
|
774
208
|
const hasStroke = Array.isArray(frame.strokes) && frame.strokes.length > 0;
|
|
775
|
-
const
|
|
209
|
+
const baseStrokeWeight = typeof frame.strokeWeight === 'number' ? frame.strokeWeight : 0;
|
|
210
|
+
const baseWeight = hasStroke && Number.isFinite(baseStrokeWeight) ? baseStrokeWeight : 0;
|
|
211
|
+
const sideWeight = (value: number | symbol): number =>
|
|
212
|
+
typeof value === 'number' && Number.isFinite(value) ? value : baseWeight;
|
|
776
213
|
return {
|
|
777
|
-
top:
|
|
778
|
-
right:
|
|
779
|
-
bottom:
|
|
780
|
-
left:
|
|
214
|
+
top: sideWeight(frame.strokeTopWeight),
|
|
215
|
+
right: sideWeight(frame.strokeRightWeight),
|
|
216
|
+
bottom: sideWeight(frame.strokeBottomWeight),
|
|
217
|
+
left: sideWeight(frame.strokeLeftWeight),
|
|
781
218
|
};
|
|
782
219
|
}
|
|
783
220
|
|
|
784
221
|
function setBorderSideWeights(frame: FrameNode, sides: { top: number; right: number; bottom: number; left: number }): void {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
);
|
|
792
|
-
if (hasIndividualStrokes) {
|
|
793
|
-
target.strokeTopWeight = sides.top;
|
|
794
|
-
target.strokeRightWeight = sides.right;
|
|
795
|
-
target.strokeBottomWeight = sides.bottom;
|
|
796
|
-
target.strokeLeftWeight = sides.left;
|
|
797
|
-
// Do not write strokeWeight here: in Figma this can normalize all sides
|
|
798
|
-
// and undo directional borders (e.g. border-t becoming full box).
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
frame.strokeWeight = Math.max(sides.top, sides.right, sides.bottom, sides.left, 0);
|
|
222
|
+
frame.strokeTopWeight = sides.top;
|
|
223
|
+
frame.strokeRightWeight = sides.right;
|
|
224
|
+
frame.strokeBottomWeight = sides.bottom;
|
|
225
|
+
frame.strokeLeftWeight = sides.left;
|
|
226
|
+
// Do not write strokeWeight here: in Figma this can normalize all sides
|
|
227
|
+
// and undo directional borders (e.g. border-t becoming full box).
|
|
802
228
|
}
|
|
803
229
|
|
|
804
230
|
function setDirectionalBorder(frame: FrameNode, utility: string, weight: number): boolean {
|
|
@@ -880,26 +306,50 @@ function applyCssDeclarationsToFrame(frame: FrameNode, declarations: Record<stri
|
|
|
880
306
|
let autoMarginLeft = false;
|
|
881
307
|
let autoMarginRight = false;
|
|
882
308
|
let autoMarginInline = false;
|
|
309
|
+
let declaredGridColumns: number | null = null;
|
|
310
|
+
|
|
311
|
+
const gridTemplateValue = declarations['grid-template-columns'];
|
|
312
|
+
if (typeof gridTemplateValue === 'string') {
|
|
313
|
+
const match = gridTemplateValue.trim().match(/repeat\((\d+),/);
|
|
314
|
+
if (match) {
|
|
315
|
+
const parsed = parseInt(match[1], 10);
|
|
316
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
317
|
+
declaredGridColumns = parsed;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
883
321
|
|
|
884
322
|
for (const [prop, rawValue] of Object.entries(declarations)) {
|
|
885
323
|
const value = rawValue.trim();
|
|
886
324
|
if (prop === 'display' && (value === 'flex' || value === 'inline-flex')) {
|
|
887
325
|
frame.layoutMode = 'HORIZONTAL';
|
|
326
|
+
// CSS default for flex containers is `align-items: stretch`.
|
|
327
|
+
setFrameCrossAlign(frame, 'STRETCH');
|
|
888
328
|
continue;
|
|
889
329
|
}
|
|
890
|
-
if (prop === 'display' && value === 'grid') {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
(frame
|
|
330
|
+
if (prop === 'display' && (value === 'grid' || value === 'inline-grid')) {
|
|
331
|
+
if (declaredGridColumns != null) {
|
|
332
|
+
frame.layoutMode = 'HORIZONTAL';
|
|
333
|
+
markGridColumnsNode(frame, declaredGridColumns);
|
|
334
|
+
if (frame.layoutWrap !== undefined) {
|
|
335
|
+
frame.layoutWrap = 'WRAP';
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
// CSS grid without explicit template columns behaves like a single-column
|
|
339
|
+
// vertical flow in our Figma mapping.
|
|
340
|
+
frame.layoutMode = 'VERTICAL';
|
|
341
|
+
markCssGridVerticalFrame(frame);
|
|
894
342
|
}
|
|
343
|
+
// CSS default for grid containers is `align-items: stretch` per track.
|
|
344
|
+
setFrameCrossAlign(frame, 'STRETCH');
|
|
895
345
|
continue;
|
|
896
346
|
}
|
|
897
347
|
if (prop === 'flex-direction') {
|
|
898
348
|
frame.layoutMode = value === 'column' || value === 'column-reverse' ? 'VERTICAL' : 'HORIZONTAL';
|
|
899
349
|
continue;
|
|
900
350
|
}
|
|
901
|
-
if (prop === 'flex-wrap' &&
|
|
902
|
-
|
|
351
|
+
if (prop === 'flex-wrap' && frame.layoutWrap !== undefined) {
|
|
352
|
+
frame.layoutWrap = value === 'wrap' || value === 'wrap-reverse' ? 'WRAP' : 'NO_WRAP';
|
|
903
353
|
continue;
|
|
904
354
|
}
|
|
905
355
|
if (prop === 'grid-template-columns') {
|
|
@@ -923,10 +373,11 @@ function applyCssDeclarationsToFrame(frame: FrameNode, declarations: Record<stri
|
|
|
923
373
|
continue;
|
|
924
374
|
}
|
|
925
375
|
if (prop === 'align-items') {
|
|
926
|
-
if (value === 'center') frame.counterAxisAlignItems = 'CENTER';
|
|
927
|
-
if (value === 'flex-start') frame.counterAxisAlignItems = 'MIN';
|
|
928
|
-
if (value === 'flex-end') frame.counterAxisAlignItems = 'MAX';
|
|
929
|
-
|
|
376
|
+
if (value === 'center') { frame.counterAxisAlignItems = 'CENTER'; setFrameCrossAlign(frame, 'CENTER'); }
|
|
377
|
+
if (value === 'flex-start') { frame.counterAxisAlignItems = 'MIN'; setFrameCrossAlign(frame, 'MIN'); }
|
|
378
|
+
if (value === 'flex-end') { frame.counterAxisAlignItems = 'MAX'; setFrameCrossAlign(frame, 'MAX'); }
|
|
379
|
+
// Figma has no parent-level STRETCH — emulated per-child via layoutAlign in applyChildProperties.
|
|
380
|
+
if (value === 'stretch') { frame.counterAxisAlignItems = 'MIN'; setFrameCrossAlign(frame, 'STRETCH'); }
|
|
930
381
|
continue;
|
|
931
382
|
}
|
|
932
383
|
if (prop === 'align-self') {
|
|
@@ -1014,6 +465,7 @@ function applyCssDeclarationsToFrame(frame: FrameNode, declarations: Record<stri
|
|
|
1014
465
|
const len = parseCssLength(value);
|
|
1015
466
|
if (len != null) {
|
|
1016
467
|
frame.resize(frame.width, len);
|
|
468
|
+
markFixedHeightNode(frame);
|
|
1017
469
|
if (frame.layoutMode === 'VERTICAL') {
|
|
1018
470
|
frame.primaryAxisSizingMode = 'FIXED';
|
|
1019
471
|
} else if (frame.layoutMode === 'HORIZONTAL') {
|
|
@@ -1077,6 +529,7 @@ function applyCssDeclarationsToFrame(frame: FrameNode, declarations: Record<stri
|
|
|
1077
529
|
|
|
1078
530
|
if (autoMarginInline || (autoMarginLeft && autoMarginRight)) {
|
|
1079
531
|
markSelfAlignmentNode(frame, 'CENTER');
|
|
532
|
+
markFullWidthNode(frame);
|
|
1080
533
|
} else if (autoMarginLeft) {
|
|
1081
534
|
markSelfAlignmentNode(frame, 'MAX');
|
|
1082
535
|
} else if (autoMarginRight) {
|
|
@@ -1374,6 +827,12 @@ function applySemanticUtilitiesToFrame(
|
|
|
1374
827
|
radiusGroup: Record<string, string> | null,
|
|
1375
828
|
): Set<string> {
|
|
1376
829
|
const handled = new Set<string>();
|
|
830
|
+
// Track whether flex-direction was explicitly set on this frame. `flex`
|
|
831
|
+
// means "enable flex layout, default direction row" — but in CSS it does
|
|
832
|
+
// NOT reset an already-set flex-direction. A duplicated class list like
|
|
833
|
+
// `flex flex-col … flex flex-col` (base + user override on SheetContent)
|
|
834
|
+
// must keep the VERTICAL direction even if the trailing `flex` is processed.
|
|
835
|
+
let flexDirectionSet = false;
|
|
1377
836
|
const spacingScale = (COMPONENT_DEFS && COMPONENT_DEFS.spacingScale) ? COMPONENT_DEFS.spacingScale : {};
|
|
1378
837
|
let gap: number | null = null;
|
|
1379
838
|
let gapX: number | null = null;
|
|
@@ -1403,8 +862,46 @@ function applySemanticUtilitiesToFrame(
|
|
|
1403
862
|
|
|
1404
863
|
if (!shouldApplyAtom(atom, 'default')) continue;
|
|
1405
864
|
|
|
1406
|
-
if (utility === 'flex' || utility === 'inline-flex'
|
|
865
|
+
if (utility === 'flex' || utility === 'inline-flex') {
|
|
866
|
+
// Bare `flex` enables flex layout but does not set direction. Only write
|
|
867
|
+
// HORIZONTAL when the direction hasn't been declared yet by a prior
|
|
868
|
+
// flex-row / flex-col / grid / flex-col-reverse etc.
|
|
869
|
+
if (!flexDirectionSet) frame.layoutMode = 'HORIZONTAL';
|
|
870
|
+
// CSS default for flex containers is `align-items: stretch`.
|
|
871
|
+
setFrameCrossAlign(frame, 'STRETCH');
|
|
872
|
+
// CSS distinguishes `display: flex` (block-level — takes full width in
|
|
873
|
+
// its parent block context) from `display: inline-flex` (inline-level
|
|
874
|
+
// — hugs content). The plugin treats both as HORIZONTAL auto-layout,
|
|
875
|
+
// but the SIZING of the container in its parent differs. Mark
|
|
876
|
+
// inline-flex frames so they hug content on their main axis and don't
|
|
877
|
+
// get stretched by a parent's STRETCH cross-axis alignment.
|
|
878
|
+
if (utility === 'inline-flex') {
|
|
879
|
+
try {
|
|
880
|
+
if ('primaryAxisSizingMode' in frame) frame.primaryAxisSizingMode = 'AUTO';
|
|
881
|
+
if ('counterAxisSizingMode' in frame) frame.counterAxisSizingMode = 'AUTO';
|
|
882
|
+
if ('layoutAlign' in frame) frame.layoutAlign = 'INHERIT';
|
|
883
|
+
if ('layoutSizingHorizontal' in frame) frame.layoutSizingHorizontal = 'HUG';
|
|
884
|
+
if ('layoutSizingVertical' in frame) frame.layoutSizingVertical = 'HUG';
|
|
885
|
+
} catch (_err) {
|
|
886
|
+
// ignore — these properties may not be writable in all node types
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
handled.add(cls);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (utility === 'flex-row' || utility === 'flex-row-reverse') {
|
|
1407
893
|
frame.layoutMode = 'HORIZONTAL';
|
|
894
|
+
flexDirectionSet = true;
|
|
895
|
+
setFrameCrossAlign(frame, 'STRETCH');
|
|
896
|
+
handled.add(cls);
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (utility === 'flex-col' || utility === 'flex-col-reverse') {
|
|
900
|
+
// `flex-col` sets column direction. Any following bare `flex` must not
|
|
901
|
+
// revert layoutMode to HORIZONTAL (see duplicated SheetContent classes).
|
|
902
|
+
frame.layoutMode = 'VERTICAL';
|
|
903
|
+
flexDirectionSet = true;
|
|
904
|
+
setFrameCrossAlign(frame, 'STRETCH');
|
|
1408
905
|
handled.add(cls);
|
|
1409
906
|
continue;
|
|
1410
907
|
}
|
|
@@ -1412,14 +909,16 @@ function applySemanticUtilitiesToFrame(
|
|
|
1412
909
|
const hasColumns = classes.some((c: string) => /^grid-cols-\d+$/.test(c));
|
|
1413
910
|
if (hasColumns) {
|
|
1414
911
|
frame.layoutMode = 'HORIZONTAL';
|
|
1415
|
-
if (
|
|
1416
|
-
|
|
912
|
+
if (frame.layoutWrap !== undefined) {
|
|
913
|
+
frame.layoutWrap = 'WRAP';
|
|
1417
914
|
}
|
|
1418
915
|
} else {
|
|
1419
916
|
// Single-column implicit grid → VERTICAL; children stretch to fill (CSS default align-items: stretch)
|
|
1420
917
|
frame.layoutMode = 'VERTICAL';
|
|
1421
|
-
|
|
918
|
+
markCssGridVerticalFrame(frame);
|
|
1422
919
|
}
|
|
920
|
+
flexDirectionSet = true;
|
|
921
|
+
setFrameCrossAlign(frame, 'STRETCH');
|
|
1423
922
|
handled.add(cls);
|
|
1424
923
|
continue;
|
|
1425
924
|
}
|
|
@@ -1499,6 +998,109 @@ function applySemanticUtilitiesToFrame(
|
|
|
1499
998
|
continue;
|
|
1500
999
|
}
|
|
1501
1000
|
|
|
1001
|
+
// Handle percent-based positions: `top-[X%]`, `bottom-[X%]`, `left-[X%]`,
|
|
1002
|
+
// `right-[X%]` (e.g. `top-[30%]`, `left-[18.33%]`). Stored on
|
|
1003
|
+
// POSITION_INFO_NODES as a fraction in [0, 1]; resolved by
|
|
1004
|
+
// `applyDeferredPercentPositioning` once the parent's dimensions are final.
|
|
1005
|
+
const topPctMatch = utility.match(/^top-\[(\d+(?:\.\d+)?)%\]$/);
|
|
1006
|
+
if (topPctMatch) {
|
|
1007
|
+
markPositionInfo(frame, { topPercent: parseFloat(topPctMatch[1]) / 100 });
|
|
1008
|
+
handled.add(cls);
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const bottomPctMatch = utility.match(/^bottom-\[(\d+(?:\.\d+)?)%\]$/);
|
|
1012
|
+
if (bottomPctMatch) {
|
|
1013
|
+
markPositionInfo(frame, { bottomPercent: parseFloat(bottomPctMatch[1]) / 100 });
|
|
1014
|
+
handled.add(cls);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const leftPctMatch = utility.match(/^left-\[(\d+(?:\.\d+)?)%\]$/);
|
|
1018
|
+
if (leftPctMatch) {
|
|
1019
|
+
markPositionInfo(frame, { leftPercent: parseFloat(leftPctMatch[1]) / 100 });
|
|
1020
|
+
handled.add(cls);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const rightPctMatch = utility.match(/^right-\[(\d+(?:\.\d+)?)%\]$/);
|
|
1024
|
+
if (rightPctMatch) {
|
|
1025
|
+
markPositionInfo(frame, { rightPercent: parseFloat(rightPctMatch[1]) / 100 });
|
|
1026
|
+
handled.add(cls);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Handle self-translate fractions: `-translate-x-1/2`, `translate-x-1/2`,
|
|
1031
|
+
// `-translate-x-full`, `translate-y-1/3`, etc. Mirrors CSS
|
|
1032
|
+
// `transform: translate(N%, N%)` applied as a fraction of the element's
|
|
1033
|
+
// OWN width/height. Most common with absolute + percent positions
|
|
1034
|
+
// (`absolute left-[50%] top-[50%] -translate-x-1/2 -translate-y-1/2`
|
|
1035
|
+
// centers the element on the parent's (50%, 50%) point). Resolved in
|
|
1036
|
+
// `applyDeferredPercentPositioning` because it depends on the child's
|
|
1037
|
+
// final width / height, which may not be known when classes are parsed.
|
|
1038
|
+
const translateMatch = utility.match(/^(-?)translate-(x|y)-(.+)$/);
|
|
1039
|
+
if (translateMatch) {
|
|
1040
|
+
const sign = translateMatch[1] === '-' ? -1 : 1;
|
|
1041
|
+
const axis = translateMatch[2];
|
|
1042
|
+
const token = translateMatch[3];
|
|
1043
|
+
let fraction: number | null = null;
|
|
1044
|
+
if (token === 'full') fraction = 1;
|
|
1045
|
+
else if (token === 'px') fraction = 0; // px-based translate is 0% of self
|
|
1046
|
+
else {
|
|
1047
|
+
const frac = token.match(/^(\d+)\/(\d+)$/);
|
|
1048
|
+
if (frac) {
|
|
1049
|
+
const num = parseFloat(frac[1]);
|
|
1050
|
+
const den = parseFloat(frac[2]);
|
|
1051
|
+
if (den !== 0) fraction = num / den;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (fraction != null) {
|
|
1055
|
+
const signed = sign * fraction;
|
|
1056
|
+
if (axis === 'x') {
|
|
1057
|
+
markPositionInfo(frame, { translateXFraction: signed });
|
|
1058
|
+
} else {
|
|
1059
|
+
markPositionInfo(frame, { translateYFraction: signed });
|
|
1060
|
+
}
|
|
1061
|
+
handled.add(cls);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Handle aspect-ratio: aspect-{n}/{m}, aspect-square, aspect-video,
|
|
1067
|
+
// aspect-[N/M] and aspect-[N] arbitrary values. Records the ratio
|
|
1068
|
+
// (width/height) on the frame; deferred-layout resolves it once the
|
|
1069
|
+
// frame's width is settled.
|
|
1070
|
+
if (utility === 'aspect-square') {
|
|
1071
|
+
markAspectRatio(frame, 1);
|
|
1072
|
+
handled.add(cls);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (utility === 'aspect-video') {
|
|
1076
|
+
markAspectRatio(frame, 16 / 9);
|
|
1077
|
+
handled.add(cls);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
const aspectFractionMatch = utility.match(/^aspect-(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)$/);
|
|
1081
|
+
if (aspectFractionMatch) {
|
|
1082
|
+
const aw = parseFloat(aspectFractionMatch[1]);
|
|
1083
|
+
const ah = parseFloat(aspectFractionMatch[2]);
|
|
1084
|
+
if (aw > 0 && ah > 0) markAspectRatio(frame, aw / ah);
|
|
1085
|
+
handled.add(cls);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const aspectArbMatch = utility.match(/^aspect-\[(.+)\]$/);
|
|
1089
|
+
if (aspectArbMatch) {
|
|
1090
|
+
const expr = aspectArbMatch[1];
|
|
1091
|
+
const slash = expr.match(/^(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/);
|
|
1092
|
+
if (slash) {
|
|
1093
|
+
const aw = parseFloat(slash[1]);
|
|
1094
|
+
const ah = parseFloat(slash[2]);
|
|
1095
|
+
if (aw > 0 && ah > 0) markAspectRatio(frame, aw / ah);
|
|
1096
|
+
} else {
|
|
1097
|
+
const num = parseFloat(expr);
|
|
1098
|
+
if (Number.isFinite(num) && num > 0) markAspectRatio(frame, num);
|
|
1099
|
+
}
|
|
1100
|
+
handled.add(cls);
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1502
1104
|
// Handle blur effects (blur-sm, blur, blur-md, blur-lg, blur-xl, blur-2xl, blur-3xl)
|
|
1503
1105
|
const blurMatch = utility.match(/^blur(?:-(.+))?$/);
|
|
1504
1106
|
if (blurMatch) {
|
|
@@ -1517,10 +1119,9 @@ function applySemanticUtilitiesToFrame(
|
|
|
1517
1119
|
const blurKey = blurMatch[1] || '';
|
|
1518
1120
|
const blurRadius = blurScale[blurKey];
|
|
1519
1121
|
if (blurRadius != null && blurRadius > 0) {
|
|
1520
|
-
frame.effects = [
|
|
1521
|
-
...(frame.effects || []),
|
|
1122
|
+
frame.effects = (frame.effects || []).concat([
|
|
1522
1123
|
{ type: 'LAYER_BLUR', blurType: 'NORMAL', radius: blurRadius, visible: true } as Effect,
|
|
1523
|
-
];
|
|
1124
|
+
]);
|
|
1524
1125
|
}
|
|
1525
1126
|
handled.add(cls);
|
|
1526
1127
|
continue;
|
|
@@ -1554,12 +1155,21 @@ function applySemanticUtilitiesToFrame(
|
|
|
1554
1155
|
};
|
|
1555
1156
|
const defs = SHADOW_MAP[shadowKey];
|
|
1556
1157
|
if (defs !== undefined) {
|
|
1158
|
+
// Figma renders multi-layer DROP_SHADOW effects as independently
|
|
1159
|
+
// stacked filters — they composite "harder" than CSS box-shadow,
|
|
1160
|
+
// where the browser's filter pipeline smooths overlapping
|
|
1161
|
+
// shadow layers. The numeric values in SHADOW_MAP match
|
|
1162
|
+
// Tailwind v3's docs exactly, but the visual result reads as
|
|
1163
|
+
// ~30 % stronger in Figma at parity. Damp the alpha channel so
|
|
1164
|
+
// the rendered page matches Storybook / browser intensity. Tune
|
|
1165
|
+
// this single constant to dial all tiers up or down together.
|
|
1166
|
+
const SHADOW_ALPHA_DAMPER = 0.7;
|
|
1557
1167
|
const shadowEffects: Effect[] = [];
|
|
1558
1168
|
for (let si = 0; si < defs.length; si++) {
|
|
1559
1169
|
const d = defs[si];
|
|
1560
1170
|
shadowEffects.push({
|
|
1561
1171
|
type: d.type,
|
|
1562
|
-
color: { r: d.r, g: d.g, b: d.b, a: d.a },
|
|
1172
|
+
color: { r: d.r, g: d.g, b: d.b, a: d.a * SHADOW_ALPHA_DAMPER },
|
|
1563
1173
|
offset: { x: d.x, y: d.y },
|
|
1564
1174
|
radius: d.radius,
|
|
1565
1175
|
spread: d.spread,
|
|
@@ -1568,23 +1178,18 @@ function applySemanticUtilitiesToFrame(
|
|
|
1568
1178
|
} as Effect);
|
|
1569
1179
|
}
|
|
1570
1180
|
// Replace any existing shadow effects (drop + inner) and keep other effects (blur etc.)
|
|
1571
|
-
const allEffects: Effect[] = Array.from(
|
|
1572
|
-
const nonShadow = allEffects.filter(
|
|
1573
|
-
(e: Effect) =>
|
|
1181
|
+
const allEffects: Effect[] = Array.from(frame.effects || []);
|
|
1182
|
+
const nonShadow: Effect[] = allEffects.filter(
|
|
1183
|
+
(e: Effect) => e.type !== 'DROP_SHADOW' && e.type !== 'INNER_SHADOW'
|
|
1574
1184
|
);
|
|
1575
|
-
|
|
1185
|
+
frame.effects = nonShadow.concat(shadowEffects);
|
|
1576
1186
|
}
|
|
1577
1187
|
handled.add(cls);
|
|
1578
1188
|
continue;
|
|
1579
1189
|
}
|
|
1580
1190
|
|
|
1581
|
-
if (utility === 'flex-
|
|
1582
|
-
frame.
|
|
1583
|
-
handled.add(cls);
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
if (utility === 'flex-wrap' && (frame as any).layoutWrap !== undefined) {
|
|
1587
|
-
(frame as any).layoutWrap = 'WRAP';
|
|
1191
|
+
if (utility === 'flex-wrap' && frame.layoutWrap !== undefined) {
|
|
1192
|
+
frame.layoutWrap = 'WRAP';
|
|
1588
1193
|
handled.add(cls);
|
|
1589
1194
|
continue;
|
|
1590
1195
|
}
|
|
@@ -1596,15 +1201,49 @@ function applySemanticUtilitiesToFrame(
|
|
|
1596
1201
|
handled.add(cls);
|
|
1597
1202
|
continue;
|
|
1598
1203
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1204
|
+
// `text-center` / `text-left` / `text-right` on a block-flow parent
|
|
1205
|
+
// also affects horizontal alignment of inline-display children inside
|
|
1206
|
+
// it (CSS centers inline content via the parent's text-align). Set
|
|
1207
|
+
// the parent's counterAxisAlignItems so the Figma inspector reflects
|
|
1208
|
+
// the visual alignment AND inline-display children inherit it. Block
|
|
1209
|
+
// children that get layoutAlign='STRETCH' via the implicit-stretch
|
|
1210
|
+
// pass still take full width regardless of this alignment, preserving
|
|
1211
|
+
// the headline/paragraph stretch-with-text-align behaviour.
|
|
1212
|
+
//
|
|
1213
|
+
// Don't touch FRAME_CROSS_ALIGN — the implicit-stretch logic uses
|
|
1214
|
+
// that to decide which children stretch; setting it to CENTER here
|
|
1215
|
+
// would disable stretching for block children of `text-center` divs.
|
|
1216
|
+
//
|
|
1217
|
+
// Doesn't `continue` — text-center also affects text rendering and is
|
|
1218
|
+
// consumed by the text builder later.
|
|
1219
|
+
if (utility === 'text-center') {
|
|
1220
|
+
setFrameInlineAlign(frame, 'CENTER');
|
|
1221
|
+
if ('counterAxisAlignItems' in frame) {
|
|
1222
|
+
try { frame.counterAxisAlignItems = 'CENTER'; } catch (_e) { /* ignore */ }
|
|
1223
|
+
}
|
|
1224
|
+
} else if (utility === 'text-right') {
|
|
1225
|
+
setFrameInlineAlign(frame, 'MAX');
|
|
1226
|
+
if ('counterAxisAlignItems' in frame) {
|
|
1227
|
+
try { frame.counterAxisAlignItems = 'MAX'; } catch (_e) { /* ignore */ }
|
|
1228
|
+
}
|
|
1229
|
+
} else if (utility === 'text-left') {
|
|
1230
|
+
setFrameInlineAlign(frame, 'MIN');
|
|
1231
|
+
if ('counterAxisAlignItems' in frame) {
|
|
1232
|
+
try { frame.counterAxisAlignItems = 'MIN'; } catch (_e) { /* ignore */ }
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (utility === 'items-center') { frame.counterAxisAlignItems = 'CENTER'; setFrameCrossAlign(frame, 'CENTER'); handled.add(cls); continue; }
|
|
1236
|
+
if (utility === 'items-start') { frame.counterAxisAlignItems = 'MIN'; setFrameCrossAlign(frame, 'MIN'); handled.add(cls); continue; }
|
|
1237
|
+
if (utility === 'items-end') { frame.counterAxisAlignItems = 'MAX'; setFrameCrossAlign(frame, 'MAX'); handled.add(cls); continue; }
|
|
1238
|
+
// Figma has no parent-level STRETCH — emulated per-child via layoutAlign in applyChildProperties.
|
|
1239
|
+
if (utility === 'items-stretch') { frame.counterAxisAlignItems = 'MIN'; setFrameCrossAlign(frame, 'STRETCH'); handled.add(cls); continue; }
|
|
1602
1240
|
if (utility === 'justify-center') { frame.primaryAxisAlignItems = 'CENTER'; handled.add(cls); continue; }
|
|
1603
1241
|
if (utility === 'justify-between') { frame.primaryAxisAlignItems = 'SPACE_BETWEEN'; handled.add(cls); continue; }
|
|
1604
1242
|
if (utility === 'justify-start') { frame.primaryAxisAlignItems = 'MIN'; handled.add(cls); continue; }
|
|
1605
1243
|
if (utility === 'justify-end') { frame.primaryAxisAlignItems = 'MAX'; handled.add(cls); continue; }
|
|
1606
1244
|
if (utility === 'mx-auto') {
|
|
1607
|
-
|
|
1245
|
+
markSelfAlignmentNode(frame, 'CENTER');
|
|
1246
|
+
markFullWidthNode(frame);
|
|
1608
1247
|
handled.add(cls);
|
|
1609
1248
|
continue;
|
|
1610
1249
|
}
|
|
@@ -1716,12 +1355,28 @@ function applySemanticUtilitiesToFrame(
|
|
|
1716
1355
|
handled.add(cls);
|
|
1717
1356
|
continue;
|
|
1718
1357
|
}
|
|
1358
|
+
if (utility === 'w-fit' || utility === 'w-auto' || utility === 'w-max') {
|
|
1359
|
+
// Hug content width — counterAxisSizingMode AUTO for vertical layout,
|
|
1360
|
+
// primaryAxisSizingMode AUTO for horizontal layout.
|
|
1361
|
+
try {
|
|
1362
|
+
if (frame.layoutMode === 'HORIZONTAL') {
|
|
1363
|
+
frame.primaryAxisSizingMode = 'AUTO';
|
|
1364
|
+
} else {
|
|
1365
|
+
frame.counterAxisSizingMode = 'AUTO';
|
|
1366
|
+
}
|
|
1367
|
+
} catch (_err) { /* ignore */ }
|
|
1368
|
+
handled.add(cls);
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1719
1371
|
if (utility === 'container') {
|
|
1720
1372
|
markFullWidthNode(frame);
|
|
1721
1373
|
handled.add(cls);
|
|
1722
1374
|
continue;
|
|
1723
1375
|
}
|
|
1724
|
-
if (utility === 'h-full') {
|
|
1376
|
+
if (utility === 'h-full' || utility === 'h-screen' || utility === 'min-h-screen') {
|
|
1377
|
+
// h-screen / min-h-screen are treated like h-full: fill parent primary axis.
|
|
1378
|
+
// The Story Layout viewport-height pre-pass pins a fixed height upstream so
|
|
1379
|
+
// the fill cascades correctly.
|
|
1725
1380
|
markFullHeightNode(frame);
|
|
1726
1381
|
handled.add(cls);
|
|
1727
1382
|
continue;
|
|
@@ -1745,9 +1400,20 @@ function applySemanticUtilitiesToFrame(
|
|
|
1745
1400
|
}
|
|
1746
1401
|
}
|
|
1747
1402
|
} else {
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1403
|
+
// Arbitrary percent — `w-[60%]` — same parent-relative semantics as
|
|
1404
|
+
// a w-1/2 / w-2/3 fraction. The post-append pipeline resizes the
|
|
1405
|
+
// child to fraction * parent content-width.
|
|
1406
|
+
const percentMatch = token.match(/^\[(\d+(?:\.\d+)?)%\]$/);
|
|
1407
|
+
if (percentMatch) {
|
|
1408
|
+
const pct = parseFloat(percentMatch[1]);
|
|
1409
|
+
if (Number.isFinite(pct) && pct >= 0) {
|
|
1410
|
+
markFractionWidthNode(frame, Math.max(0, Math.min(1, pct / 100)));
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
const fraction = parseFractionToken(token);
|
|
1414
|
+
if (fraction != null) {
|
|
1415
|
+
markFractionWidthNode(frame, fraction);
|
|
1416
|
+
}
|
|
1751
1417
|
}
|
|
1752
1418
|
}
|
|
1753
1419
|
handled.add(cls);
|
|
@@ -1758,6 +1424,7 @@ function applySemanticUtilitiesToFrame(
|
|
|
1758
1424
|
const val = resolveSpacingToken(hMatch[1], spacingScale);
|
|
1759
1425
|
if (val != null) {
|
|
1760
1426
|
frame.resize(frame.width, val);
|
|
1427
|
+
markFixedHeightNode(frame);
|
|
1761
1428
|
// Set sizing mode to FIXED to prevent auto-resizing
|
|
1762
1429
|
if (frame.layoutMode === 'VERTICAL') {
|
|
1763
1430
|
frame.primaryAxisSizingMode = 'FIXED';
|
|
@@ -1774,6 +1441,64 @@ function applySemanticUtilitiesToFrame(
|
|
|
1774
1441
|
continue;
|
|
1775
1442
|
}
|
|
1776
1443
|
|
|
1444
|
+
// min-h-N / min-h-[Npx] → minHeight floor. Used by shadcn Textarea
|
|
1445
|
+
// (`min-h-20`) and any other consumer who wants a height baseline.
|
|
1446
|
+
// `min-h-screen` is handled separately upstream as a FILL signal.
|
|
1447
|
+
const minHScaleMatch = utility.match(/^min-h-(\d+(?:\.\d+)?)$/);
|
|
1448
|
+
if (minHScaleMatch) {
|
|
1449
|
+
const val = resolveSpacingToken(minHScaleMatch[1], spacingScale);
|
|
1450
|
+
if (val != null && val > 0) {
|
|
1451
|
+
try {
|
|
1452
|
+
if ('minHeight' in frame) (frame as { minHeight: number }).minHeight = val;
|
|
1453
|
+
if (frame.height < val) frame.resize(frame.width, val);
|
|
1454
|
+
} catch (_e) { /* ignore */ }
|
|
1455
|
+
}
|
|
1456
|
+
handled.add(cls);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const minHArbitraryMatch = utility.match(/^min-h-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
|
|
1460
|
+
if (minHArbitraryMatch) {
|
|
1461
|
+
let val = parseFloat(minHArbitraryMatch[1]);
|
|
1462
|
+
const unit = minHArbitraryMatch[2] || 'px';
|
|
1463
|
+
if (unit === 'rem' || unit === 'em') val *= 16;
|
|
1464
|
+
if (Number.isFinite(val) && val > 0) {
|
|
1465
|
+
try {
|
|
1466
|
+
if ('minHeight' in frame) (frame as { minHeight: number }).minHeight = val;
|
|
1467
|
+
if (frame.height < val) frame.resize(frame.width, val);
|
|
1468
|
+
} catch (_e) { /* ignore */ }
|
|
1469
|
+
}
|
|
1470
|
+
handled.add(cls);
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// min-w-N / min-w-[Npx] — mirror of min-h-*.
|
|
1475
|
+
const minWScaleMatch = utility.match(/^min-w-(\d+(?:\.\d+)?)$/);
|
|
1476
|
+
if (minWScaleMatch) {
|
|
1477
|
+
const val = resolveSpacingToken(minWScaleMatch[1], spacingScale);
|
|
1478
|
+
if (val != null && val > 0) {
|
|
1479
|
+
try {
|
|
1480
|
+
if ('minWidth' in frame) (frame as { minWidth: number }).minWidth = val;
|
|
1481
|
+
if (frame.width < val) frame.resize(val, frame.height);
|
|
1482
|
+
} catch (_e) { /* ignore */ }
|
|
1483
|
+
}
|
|
1484
|
+
handled.add(cls);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
const minWArbitraryMatch = utility.match(/^min-w-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
|
|
1488
|
+
if (minWArbitraryMatch) {
|
|
1489
|
+
let val = parseFloat(minWArbitraryMatch[1]);
|
|
1490
|
+
const unit = minWArbitraryMatch[2] || 'px';
|
|
1491
|
+
if (unit === 'rem' || unit === 'em') val *= 16;
|
|
1492
|
+
if (Number.isFinite(val) && val > 0) {
|
|
1493
|
+
try {
|
|
1494
|
+
if ('minWidth' in frame) (frame as { minWidth: number }).minWidth = val;
|
|
1495
|
+
if (frame.width < val) frame.resize(val, frame.height);
|
|
1496
|
+
} catch (_e) { /* ignore */ }
|
|
1497
|
+
}
|
|
1498
|
+
handled.add(cls);
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1777
1502
|
// size-X sets both width and height (Tailwind's size utility)
|
|
1778
1503
|
const sizeMatch = utility.match(/^size-(.+)$/);
|
|
1779
1504
|
if (sizeMatch) {
|
|
@@ -1781,6 +1506,7 @@ function applySemanticUtilitiesToFrame(
|
|
|
1781
1506
|
if (val != null) {
|
|
1782
1507
|
frame.resize(val, val);
|
|
1783
1508
|
markFixedWidthNode(frame);
|
|
1509
|
+
markFixedHeightNode(frame);
|
|
1784
1510
|
frame.primaryAxisSizingMode = 'FIXED';
|
|
1785
1511
|
frame.counterAxisSizingMode = 'FIXED';
|
|
1786
1512
|
}
|
|
@@ -1793,6 +1519,7 @@ function applySemanticUtilitiesToFrame(
|
|
|
1793
1519
|
if (maxW != null) {
|
|
1794
1520
|
frame.resize(maxW, frame.height);
|
|
1795
1521
|
frame.counterAxisSizingMode = 'FIXED';
|
|
1522
|
+
markMaxWidthNode(frame, maxW);
|
|
1796
1523
|
}
|
|
1797
1524
|
handled.add(cls);
|
|
1798
1525
|
continue;
|
|
@@ -1858,11 +1585,17 @@ export function applyTailwindStylesToFrame(
|
|
|
1858
1585
|
): TailwindStyle {
|
|
1859
1586
|
const style = tailwindClassesToStyle(classes, 'default', colorGroup);
|
|
1860
1587
|
const styleMap = getStyleMap();
|
|
1861
|
-
applySemanticUtilitiesToFrame(frame, classes, radiusGroup);
|
|
1588
|
+
const semanticHandled = applySemanticUtilitiesToFrame(frame, classes, radiusGroup);
|
|
1862
1589
|
let hasGradient = false;
|
|
1863
1590
|
|
|
1864
1591
|
if (styleMap) {
|
|
1865
1592
|
for (const cls of classes) {
|
|
1593
|
+
// Classes resolved by the semantic pass above (grid/flex/grid-cols-*/col-span-*/…)
|
|
1594
|
+
// must not be re-applied via per-class CSS declarations. Per-class CSS is isolated:
|
|
1595
|
+
// when `grid` is processed in isolation it sees `display: grid` with no
|
|
1596
|
+
// `grid-template-columns` and resets layoutMode to VERTICAL, undoing the semantic
|
|
1597
|
+
// pass that already set HORIZONTAL + WRAP for multi-column grids.
|
|
1598
|
+
if (semanticHandled.has(cls)) continue;
|
|
1866
1599
|
const entryList = styleMap[cls];
|
|
1867
1600
|
if (!entryList || entryList.length === 0) continue;
|
|
1868
1601
|
const atom = parseUtilityClass(cls);
|
|
@@ -2010,6 +1743,18 @@ export function applyTailwindStylesToFrame(
|
|
|
2010
1743
|
frame.opacity = style.opacity;
|
|
2011
1744
|
}
|
|
2012
1745
|
|
|
1746
|
+
// Ring utilities (`ring-1`, `ring-2`, `ring-ring`, `ring-primary/50`,
|
|
1747
|
+
// `ring-offset-N`, …) are MARKED here at parse time and resolved later
|
|
1748
|
+
// by `applyRingIfPossible` in the post-append pipeline, when the host's
|
|
1749
|
+
// content children are all in place and its natural W × H is final.
|
|
1750
|
+
// Doing this eagerly here used to inflate Hug-sized parents during the
|
|
1751
|
+
// brief flex-child moment between `appendChild` and
|
|
1752
|
+
// `layoutPositioning = 'ABSOLUTE'`. See `src/layout/deferred-layout.ts`
|
|
1753
|
+
// (`applyRingIfPossible`) for why the deferred phase is the right
|
|
1754
|
+
// place for this concern.
|
|
1755
|
+
const ringInfo = getRingInfoFromClasses(classes, colorGroup);
|
|
1756
|
+
if (ringInfo) markRingNode(frame, ringInfo);
|
|
1757
|
+
|
|
2013
1758
|
return style;
|
|
2014
1759
|
}
|
|
2015
1760
|
|
|
@@ -2299,11 +2044,11 @@ export function tailwindForNode(node: SceneNode): string {
|
|
|
2299
2044
|
if (frame.counterAxisAlignItems && mapAlign[frame.counterAxisAlignItems]) {
|
|
2300
2045
|
classes.push(mapAlign[frame.counterAxisAlignItems]);
|
|
2301
2046
|
}
|
|
2302
|
-
if (
|
|
2303
|
-
if (
|
|
2047
|
+
if (frame.layoutGrow === 1) classes.push('flex-1');
|
|
2048
|
+
if (frame.layoutAlign === 'STRETCH') classes.push('self-stretch');
|
|
2304
2049
|
} else {
|
|
2305
|
-
if (
|
|
2306
|
-
if (
|
|
2050
|
+
if ('width' in node && typeof node.width === 'number') pushSizeClass(classes, 'w', node.width);
|
|
2051
|
+
if ('height' in node && typeof node.height === 'number') pushSizeClass(classes, 'h', node.height);
|
|
2307
2052
|
}
|
|
2308
2053
|
|
|
2309
2054
|
// Corner radius
|
|
@@ -2315,7 +2060,8 @@ export function tailwindForNode(node: SceneNode): string {
|
|
|
2315
2060
|
|
|
2316
2061
|
// Fills -> background or text color
|
|
2317
2062
|
try {
|
|
2318
|
-
const
|
|
2063
|
+
const fills = 'fills' in node ? node.fills : undefined;
|
|
2064
|
+
const paint = firstVisiblePaint(Array.isArray(fills) ? fills : null);
|
|
2319
2065
|
if (paint) {
|
|
2320
2066
|
const rgb = paint.color;
|
|
2321
2067
|
const token = nearestColorToken(rgb);
|
|
@@ -2325,35 +2071,38 @@ export function tailwindForNode(node: SceneNode): string {
|
|
|
2325
2071
|
classes.push(opacityClass(paint.opacity));
|
|
2326
2072
|
}
|
|
2327
2073
|
}
|
|
2328
|
-
} catch (
|
|
2074
|
+
} catch (_e) { /* ignore */ }
|
|
2329
2075
|
|
|
2330
2076
|
// Strokes -> border color/width
|
|
2331
2077
|
try {
|
|
2332
|
-
const
|
|
2078
|
+
const strokes = 'strokes' in node ? node.strokes : undefined;
|
|
2079
|
+
const paint = firstVisiblePaint(Array.isArray(strokes) ? strokes : null);
|
|
2333
2080
|
if (paint) {
|
|
2334
|
-
const
|
|
2081
|
+
const sw = 'strokeWeight' in node && typeof node.strokeWeight === 'number' ? node.strokeWeight : 1;
|
|
2082
|
+
const w = Math.round(sw || 1);
|
|
2335
2083
|
classes.push('border');
|
|
2336
2084
|
if (w !== 1) classes.push('border-' + twSpace(w));
|
|
2337
2085
|
const token = nearestColorToken(paint.color);
|
|
2338
2086
|
if (token) classes.push('border-' + token);
|
|
2339
2087
|
}
|
|
2340
|
-
} catch (
|
|
2088
|
+
} catch (_e) { /* ignore */ }
|
|
2341
2089
|
|
|
2342
2090
|
// Shadows (drop)
|
|
2343
2091
|
try {
|
|
2344
|
-
const
|
|
2092
|
+
const effects = 'effects' in node && Array.isArray(node.effects) ? node.effects : [];
|
|
2093
|
+
const eff = effects.find(
|
|
2345
2094
|
(e: Effect) => e.type === 'DROP_SHADOW' && e.visible !== false,
|
|
2346
2095
|
);
|
|
2347
2096
|
const cl = mapShadow(eff);
|
|
2348
2097
|
if (cl) classes.push(cl);
|
|
2349
|
-
} catch (
|
|
2098
|
+
} catch (_e) { /* ignore */ }
|
|
2350
2099
|
|
|
2351
2100
|
// Typography for text
|
|
2352
2101
|
if (node.type === 'TEXT') {
|
|
2353
2102
|
const textNode = node as TextNode;
|
|
2354
2103
|
if (textNode.fontSize) classes.push('text-[' + px(textNode.fontSize as number) + ']');
|
|
2355
|
-
if (textNode.lineHeight &&
|
|
2356
|
-
classes.push('leading-[' + px(
|
|
2104
|
+
if (textNode.lineHeight && typeof textNode.lineHeight === 'object' && textNode.lineHeight.unit === 'PIXELS') {
|
|
2105
|
+
classes.push('leading-[' + px(textNode.lineHeight.value) + ']');
|
|
2357
2106
|
}
|
|
2358
2107
|
if (textNode.fontName && (textNode.fontName as FontName).style && /bold/i.test((textNode.fontName as FontName).style)) {
|
|
2359
2108
|
classes.push('font-bold');
|