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,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadcn adapter — translates shadcn / base-ui runtime-CSS conventions
|
|
3
|
+
* into class-list patches the plugin can see.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ---------------
|
|
7
|
+
* shadcn primitives lean on stylesheet rules that live outside the
|
|
8
|
+
* consumer's JSX — most notably `position: absolute; inset: 0` on
|
|
9
|
+
* `[data-slot="avatar-image"]` so the image overlays the fallback when it
|
|
10
|
+
* loads. The plugin scans className strings, not CSS files, so a
|
|
11
|
+
* `<Avatar><AvatarImage /><AvatarFallback /></Avatar>` tree renders with
|
|
12
|
+
* Image + Fallback as flow siblings, fighting over 40px in a horizontal
|
|
13
|
+
* flex parent, and both collapse to invisible.
|
|
14
|
+
*
|
|
15
|
+
* The adapter recognises a small set of shadcn `data-slot` markers at the
|
|
16
|
+
* IR-transform stage and injects the missing Tailwind classes. The
|
|
17
|
+
* renderer's existing pipeline (absolute positioning, full-width fills,
|
|
18
|
+
* ...) then Just Works — no parallel code path in the renderer.
|
|
19
|
+
*
|
|
20
|
+
* Adding a new entry
|
|
21
|
+
* ------------------
|
|
22
|
+
* If a shadcn primitive renders wrong in Figma AND the fix is a small,
|
|
23
|
+
* static set of Tailwind classes the consumer's JSX would have had
|
|
24
|
+
* if runtime CSS weren't doing the job, add a `SLOT_CLASS_INJECTIONS`
|
|
25
|
+
* entry, a regression case in `shadcn-quirks-regression.ts`, and a note
|
|
26
|
+
* in `.ai/framework-adapters.md`. If the fix needs layout-time decisions
|
|
27
|
+
* (parent shape, sibling count, theme, ...) it belongs in the renderer
|
|
28
|
+
* instead — this file stays focused on "class-list patches the
|
|
29
|
+
* consumer's JSX should have had."
|
|
30
|
+
*
|
|
31
|
+
* Sibling adapters
|
|
32
|
+
* ----------------
|
|
33
|
+
* The `framework-adapters/` folder is the home for any future ports of
|
|
34
|
+
* this idea (MUI, Headless UI, Chakra, ...). Each lives in its own file
|
|
35
|
+
* and is composed via the dispatcher in `index.ts`.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Type-only import: avoid a runtime cycle with `tailwind/node-ir.ts`,
|
|
39
|
+
// which imports this file's `applyShadcnAdapter`.
|
|
40
|
+
import type { NodeIR } from '../tailwind/node-ir';
|
|
41
|
+
import { isClassedElement, mergeMissing, resolveValuePercents } from '../tailwind/adapter-utils';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `data-slot` → extra Tailwind classes injected before layout solving.
|
|
45
|
+
*
|
|
46
|
+
* - `avatar-image`: base-ui's stylesheet pins it `position: absolute;
|
|
47
|
+
* inset: 0` so the image overlays the fallback when it loads. Without
|
|
48
|
+
* the CSS, both children compete for flow space and collapse. Injecting
|
|
49
|
+
* `absolute inset-0` reproduces the overlay shape via the renderer's
|
|
50
|
+
* existing absolute-positioning pipeline.
|
|
51
|
+
*/
|
|
52
|
+
const SLOT_CLASS_INJECTIONS: Record<string, readonly string[]> = {
|
|
53
|
+
'avatar-image': ['absolute', 'inset-0'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function getDataSlot(node: NodeIR): string | null {
|
|
57
|
+
if (!isClassedElement(node)) return null;
|
|
58
|
+
const raw = node.props ? node.props['data-slot'] : undefined;
|
|
59
|
+
return typeof raw === 'string' && raw.length > 0 ? raw : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Coerce a `kind: 'component'` node to a `kind: 'element'` div, optionally
|
|
64
|
+
* with a fresh class list. Used by the Slider adapter to bypass the
|
|
65
|
+
* state-instance master lookup — Slider's def is `type='state'` (scanner
|
|
66
|
+
* heuristic mis-classifies it because of `data-[disabled]:opacity-50`),
|
|
67
|
+
* so every `<SliderPrimitive.*>` node would otherwise route through
|
|
68
|
+
* `tryRenderSharedSymbolInstance` and get an empty state-instance.
|
|
69
|
+
*
|
|
70
|
+
* Element nodes carry `tagLower` and `tagName`; we keep the original
|
|
71
|
+
* tagName so layer names still read "SliderPrimitive.Thumb" etc., but
|
|
72
|
+
* render via the plain-element path.
|
|
73
|
+
*/
|
|
74
|
+
function toElementWithClasses(node: NodeIR, classes: string[]): NodeIR {
|
|
75
|
+
if (!isClassedElement(node)) return node;
|
|
76
|
+
const sameKind = node.kind === 'element';
|
|
77
|
+
const sameClasses = classes === node.classes;
|
|
78
|
+
if (sameKind && sameClasses) return node;
|
|
79
|
+
const patch: Record<string, unknown> = { kind: 'element', classes };
|
|
80
|
+
if (!('tagLower' in node) || !(node as unknown as { tagLower?: string }).tagLower) {
|
|
81
|
+
patch.tagLower = 'div';
|
|
82
|
+
}
|
|
83
|
+
return Object.assign({}, node, patch) as NodeIR;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getShadcnSlotInjections(slot: string): readonly string[] | null {
|
|
87
|
+
return SLOT_CLASS_INJECTIONS[slot] ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tree transforms applied to an `[data-slot="avatar"]` subtree to mirror
|
|
92
|
+
* shadcn's runtime behaviour:
|
|
93
|
+
*
|
|
94
|
+
* - **Drop the fallback when the image has a real src.** In shadcn,
|
|
95
|
+
* `<AvatarFallback>` only renders if `<AvatarImage>` fails to load.
|
|
96
|
+
* We can't observe load state, but we CAN observe "is there a
|
|
97
|
+
* real-looking src on the sibling image?" at scan time. If yes, the
|
|
98
|
+
* fallback would never visibly render in the browser — drop it so
|
|
99
|
+
* Figma doesn't show a misleading initials disc behind the
|
|
100
|
+
* successfully-rendered image.
|
|
101
|
+
*
|
|
102
|
+
* If the rules grow, split this out into its own helper / module — keep
|
|
103
|
+
* the registry pattern dominant.
|
|
104
|
+
*/
|
|
105
|
+
function looksLikeAvatarImageSrc(src: unknown): boolean {
|
|
106
|
+
if (typeof src !== 'string' || src.length === 0) return false;
|
|
107
|
+
return src.startsWith('/')
|
|
108
|
+
|| src.startsWith('http://')
|
|
109
|
+
|| src.startsWith('https://')
|
|
110
|
+
|| src.startsWith('./')
|
|
111
|
+
|| src.startsWith('../');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function dropFallbackWhenAvatarImageHasSrc(
|
|
115
|
+
parent: Extract<NodeIR, { kind: 'element' | 'component' }>,
|
|
116
|
+
): NodeIR[] | null {
|
|
117
|
+
if (getDataSlot(parent) !== 'avatar') return null;
|
|
118
|
+
const children = parent.children;
|
|
119
|
+
if (!Array.isArray(children) || children.length === 0) return null;
|
|
120
|
+
|
|
121
|
+
let hasImageWithSrc = false;
|
|
122
|
+
for (const child of children) {
|
|
123
|
+
if (!isClassedElement(child)) continue;
|
|
124
|
+
if (getDataSlot(child) !== 'avatar-image') continue;
|
|
125
|
+
const src = child.props ? child.props.src : undefined;
|
|
126
|
+
if (looksLikeAvatarImageSrc(src)) {
|
|
127
|
+
hasImageWithSrc = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!hasImageWithSrc) return null;
|
|
132
|
+
|
|
133
|
+
let dropped = false;
|
|
134
|
+
const next: NodeIR[] = [];
|
|
135
|
+
for (const child of children) {
|
|
136
|
+
if (isClassedElement(child) && getDataSlot(child) === 'avatar-fallback') {
|
|
137
|
+
dropped = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
next.push(child);
|
|
141
|
+
}
|
|
142
|
+
return dropped ? next : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a shadcn Progress.Root's `value` prop to a 0-100 percent. The
|
|
147
|
+
* scanner stringifies every prop, so `<Progress value={60}>` arrives as
|
|
148
|
+
* `props.value === "60"`. `value={null}` (indeterminate) comes through
|
|
149
|
+
* as missing / undefined / "null" — for static-design purposes we
|
|
150
|
+
* surface a recognisably partial bar (33%) so Indeterminate doesn't
|
|
151
|
+
* look identical to Complete.
|
|
152
|
+
*
|
|
153
|
+
* Indeterminate visual choice: shadcn / base-ui animates the indicator
|
|
154
|
+
* sliding across the track. Figma can't animate; 33% is a recognised
|
|
155
|
+
* "in progress, no specific value" representation.
|
|
156
|
+
*/
|
|
157
|
+
const PROGRESS_INDETERMINATE_PERCENT = 33;
|
|
158
|
+
|
|
159
|
+
function resolveProgressPercent(value: unknown): number {
|
|
160
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
161
|
+
return Math.max(0, Math.min(100, value));
|
|
162
|
+
}
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
const trimmed = value.trim();
|
|
165
|
+
if (trimmed.length === 0 || trimmed === 'null' || trimmed === 'undefined') {
|
|
166
|
+
return PROGRESS_INDETERMINATE_PERCENT;
|
|
167
|
+
}
|
|
168
|
+
const parsed = parseFloat(trimmed);
|
|
169
|
+
if (Number.isFinite(parsed)) {
|
|
170
|
+
return Math.max(0, Math.min(100, parsed));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return PROGRESS_INDETERMINATE_PERCENT;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Walk the Progress subtree and inject a width class on the
|
|
178
|
+
* `progress-indicator` descendant so the bar visibly reflects the
|
|
179
|
+
* value prop. shadcn / base-ui sets this via inline `transform:
|
|
180
|
+
* translateX(-N%)` at runtime which the plugin's className-only
|
|
181
|
+
* scanner never sees.
|
|
182
|
+
*/
|
|
183
|
+
function applyProgressIndicatorWidth(
|
|
184
|
+
root: Extract<NodeIR, { kind: 'element' | 'component' }>,
|
|
185
|
+
): NodeIR | null {
|
|
186
|
+
if (getDataSlot(root) !== 'progress') return null;
|
|
187
|
+
const percent = resolveProgressPercent(root.props ? root.props.value : undefined);
|
|
188
|
+
// `w-[N%]` so the universal parser (sizing.ts) sets widthMode=FILL +
|
|
189
|
+
// widthFraction = N/100; the post-append pipeline resizes the
|
|
190
|
+
// indicator to N% of the Track's content width.
|
|
191
|
+
const widthClass = `w-[${percent}%]`;
|
|
192
|
+
|
|
193
|
+
let changed = false;
|
|
194
|
+
const inject = (n: NodeIR): NodeIR => {
|
|
195
|
+
if (!isClassedElement(n)) {
|
|
196
|
+
// Walk through ring / fragment shapes.
|
|
197
|
+
if (n.kind === 'fragment') {
|
|
198
|
+
let any = false;
|
|
199
|
+
const nx: NodeIR[] = [];
|
|
200
|
+
for (const c of n.children) {
|
|
201
|
+
const r = inject(c);
|
|
202
|
+
if (r !== c) any = true;
|
|
203
|
+
nx.push(r);
|
|
204
|
+
}
|
|
205
|
+
return any ? Object.assign({}, n, { children: nx }) : n;
|
|
206
|
+
}
|
|
207
|
+
if (n.kind === 'ring') {
|
|
208
|
+
const nc = inject(n.child);
|
|
209
|
+
return nc === n.child ? n : Object.assign({}, n, { child: nc });
|
|
210
|
+
}
|
|
211
|
+
return n;
|
|
212
|
+
}
|
|
213
|
+
let nextN: NodeIR = n;
|
|
214
|
+
if (getDataSlot(n) === 'progress-indicator') {
|
|
215
|
+
const merged = mergeMissing(n.classes ?? [], [widthClass]);
|
|
216
|
+
if (merged !== n.classes) {
|
|
217
|
+
nextN = Object.assign({}, n, { classes: merged });
|
|
218
|
+
changed = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!('children' in nextN) || !Array.isArray((nextN as { children?: unknown }).children)) {
|
|
222
|
+
return nextN;
|
|
223
|
+
}
|
|
224
|
+
let kidsChanged = false;
|
|
225
|
+
const kids = (nextN as { children: NodeIR[] }).children;
|
|
226
|
+
const nextKids: NodeIR[] = [];
|
|
227
|
+
for (const c of kids) {
|
|
228
|
+
const r = inject(c);
|
|
229
|
+
if (r !== c) kidsChanged = true;
|
|
230
|
+
nextKids.push(r);
|
|
231
|
+
}
|
|
232
|
+
return kidsChanged ? Object.assign({}, nextN, { children: nextKids }) : nextN;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const out = inject(root);
|
|
236
|
+
return changed ? out : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Walk the Slider subtree and rebuild it so the indicator + thumbs
|
|
241
|
+
* reflect the value prop:
|
|
242
|
+
*
|
|
243
|
+
* - **Indicator** gets `absolute left-[min%] w-[(max-min)%]` so the
|
|
244
|
+
* filled portion of the track matches the value range (for a
|
|
245
|
+
* single-value slider, min=0 and the indicator stretches from the
|
|
246
|
+
* left edge to the thumb position).
|
|
247
|
+
* - **Thumbs**: the scanner emits a SINGLE template even for range
|
|
248
|
+
* sliders (`{values.map(...)}` is not statically expanded). The
|
|
249
|
+
* adapter clones the template once per value and injects
|
|
250
|
+
* `absolute left-[Vi%] -translate-x-1/2` on each clone, centering
|
|
251
|
+
* it on its value point exactly like base-ui's inline styles do
|
|
252
|
+
* at runtime.
|
|
253
|
+
*/
|
|
254
|
+
function applySliderPositioning(
|
|
255
|
+
root: Extract<NodeIR, { kind: 'element' | 'component' }>,
|
|
256
|
+
): NodeIR | null {
|
|
257
|
+
if (getDataSlot(root) !== 'slider') return null;
|
|
258
|
+
const rawValue = root.props && (root.props.defaultValue ?? root.props.value);
|
|
259
|
+
const rawMax = root.props ? root.props.max : undefined;
|
|
260
|
+
// base-ui's Slider is min=0..max only; pass min=0 explicitly.
|
|
261
|
+
const pcts = resolveValuePercents(rawValue, 0, rawMax);
|
|
262
|
+
if (pcts.length === 0) return null;
|
|
263
|
+
|
|
264
|
+
// Figma plugin sandbox doesn't support spread (`...arr`); compute min/max
|
|
265
|
+
// by hand. For a single-value slider, the bar fills from 0 to that value.
|
|
266
|
+
let minPct = pcts[0];
|
|
267
|
+
let maxPct = pcts[0];
|
|
268
|
+
if (pcts.length > 1) {
|
|
269
|
+
for (let i = 1; i < pcts.length; i++) {
|
|
270
|
+
if (pcts[i] < minPct) minPct = pcts[i];
|
|
271
|
+
if (pcts[i] > maxPct) maxPct = pcts[i];
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
minPct = 0;
|
|
275
|
+
}
|
|
276
|
+
const indicatorWidth = Math.max(0, maxPct - minPct);
|
|
277
|
+
|
|
278
|
+
let touched = false;
|
|
279
|
+
|
|
280
|
+
const transform = (n: NodeIR): NodeIR | NodeIR[] => {
|
|
281
|
+
if (!isClassedElement(n)) {
|
|
282
|
+
if (n.kind === 'fragment') {
|
|
283
|
+
let any = false;
|
|
284
|
+
const out: NodeIR[] = [];
|
|
285
|
+
for (const c of n.children) {
|
|
286
|
+
const r = transform(c);
|
|
287
|
+
if (Array.isArray(r)) {
|
|
288
|
+
any = true;
|
|
289
|
+
for (let i = 0; i < r.length; i++) out.push(r[i]);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (r !== c) any = true;
|
|
293
|
+
out.push(r);
|
|
294
|
+
}
|
|
295
|
+
return any ? Object.assign({}, n, { children: out }) : n;
|
|
296
|
+
}
|
|
297
|
+
if (n.kind === 'ring') {
|
|
298
|
+
const r = transform(n.child);
|
|
299
|
+
const next = Array.isArray(r) ? r[0] : r;
|
|
300
|
+
return next === n.child ? n : Object.assign({}, n, { child: next });
|
|
301
|
+
}
|
|
302
|
+
return n;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const slot = getDataSlot(n);
|
|
306
|
+
|
|
307
|
+
// Indicator: position + width. Also convert kind='component' → 'element'
|
|
308
|
+
// so the renderer doesn't try a state-instance lookup (Slider's def is
|
|
309
|
+
// mis-classified as `state` by the scanner because of `data-[disabled]`
|
|
310
|
+
// — every sub-primitive resolves back to that same def and would try
|
|
311
|
+
// the state-master which can't represent the value-driven geometry).
|
|
312
|
+
if (slot === 'slider-indicator') {
|
|
313
|
+
const extras = [`left-[${minPct}%]`, `w-[${indicatorWidth}%]`];
|
|
314
|
+
const merged = mergeMissing(n.classes ?? [], extras);
|
|
315
|
+
const out = toElementWithClasses(n, merged !== n.classes ? merged : n.classes ?? []);
|
|
316
|
+
if (out !== n) touched = true;
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Thumb template → one clone per value, each absolutely positioned
|
|
321
|
+
// AND coerced to `kind: 'element'` for the same reason as the
|
|
322
|
+
// indicator above.
|
|
323
|
+
if (slot === 'slider-thumb') {
|
|
324
|
+
const clones: NodeIR[] = [];
|
|
325
|
+
for (const pct of pcts) {
|
|
326
|
+
const extras = ['absolute', `left-[${pct}%]`, '-translate-x-1/2'];
|
|
327
|
+
const cloneClasses = mergeMissing(n.classes ?? [], extras);
|
|
328
|
+
clones.push(toElementWithClasses(n, cloneClasses));
|
|
329
|
+
}
|
|
330
|
+
touched = true;
|
|
331
|
+
return clones;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Non-leaf slider parts (`slider`, `slider-control`, `slider-track`):
|
|
335
|
+
// coerce to element so the wrapper-frame path renders the JSX children
|
|
336
|
+
// instead of going through the state-instance lookup.
|
|
337
|
+
let nextN: NodeIR = n;
|
|
338
|
+
if (slot === 'slider' || slot === 'slider-control' || slot === 'slider-track') {
|
|
339
|
+
const coerced = toElementWithClasses(n, n.classes ?? []);
|
|
340
|
+
if (coerced !== n) {
|
|
341
|
+
nextN = coerced;
|
|
342
|
+
touched = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Recurse — and splice in any cloned children returned as arrays.
|
|
347
|
+
if (!('children' in nextN) || !Array.isArray((nextN as { children?: unknown }).children)) {
|
|
348
|
+
return nextN;
|
|
349
|
+
}
|
|
350
|
+
let kidsChanged = false;
|
|
351
|
+
const original = (nextN as { children: NodeIR[] }).children;
|
|
352
|
+
const next: NodeIR[] = [];
|
|
353
|
+
for (const c of original) {
|
|
354
|
+
const r = transform(c);
|
|
355
|
+
if (Array.isArray(r)) {
|
|
356
|
+
if (r.length !== 1 || r[0] !== c) kidsChanged = true;
|
|
357
|
+
// sandbox: no spread — push items one-by-one
|
|
358
|
+
for (let i = 0; i < r.length; i++) next.push(r[i]);
|
|
359
|
+
} else {
|
|
360
|
+
if (r !== c) kidsChanged = true;
|
|
361
|
+
next.push(r);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return kidsChanged ? Object.assign({}, nextN, { children: next }) : nextN;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const out = transform(root);
|
|
368
|
+
const single = Array.isArray(out) ? out[0] : out;
|
|
369
|
+
return touched ? single : null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Drop Radix ScrollArea control children AND flatten the Viewport
|
|
374
|
+
* wrapper. Two operations rolled into one pass because they target
|
|
375
|
+
* sibling positions inside the same ScrollAreaPrimitive.Root subtree.
|
|
376
|
+
*
|
|
377
|
+
* 1. `ScrollAreaPrimitive.Scrollbar` / `.Thumb` / `.Corner` are
|
|
378
|
+
* runtime-only behavioural overlays (scrollbar fades in while the
|
|
379
|
+
* user scrolls, Corner only appears when both axes overflow). In a
|
|
380
|
+
* static design-system render they manifest as visible content
|
|
381
|
+
* blobs inside the scroll-area. Strip them.
|
|
382
|
+
*
|
|
383
|
+
* 2. `ScrollAreaPrimitive.Viewport` is a structural wrapper Radix
|
|
384
|
+
* uses for `overflow: hidden` + scroll-position tracking — it
|
|
385
|
+
* carries no visual styling beyond `h-full w-full
|
|
386
|
+
* rounded-[inherit]`. In Figma the default frame fill (white)
|
|
387
|
+
* paints it as a second nested rounded rectangle, producing the
|
|
388
|
+
* "double card / ghost-border" effect the user reported. Flatten
|
|
389
|
+
* by pulling Viewport's children up to be Root's direct children.
|
|
390
|
+
*
|
|
391
|
+
* Returns the rewritten children array when something changed, `null`
|
|
392
|
+
* when the subtree was untouched (so callers can keep object identity
|
|
393
|
+
* stable for caching).
|
|
394
|
+
*/
|
|
395
|
+
const RADIX_SCROLL_AREA_RUNTIME_TAGS = new Set([
|
|
396
|
+
'ScrollAreaPrimitive.Scrollbar',
|
|
397
|
+
'ScrollAreaPrimitive.Thumb',
|
|
398
|
+
'ScrollAreaPrimitive.Corner',
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* JSX-tree tags the shadcn adapter unconditionally drops at render time.
|
|
403
|
+
* Exposed so pre-render scans that walk classes (e.g. responsive-signal
|
|
404
|
+
* detection) can ignore classes that won't actually reach Figma — without
|
|
405
|
+
* this, a `sm:bg-black/60` on `ScrollAreaPrimitive.Scrollbar` falsely
|
|
406
|
+
* trips `treeHasResponsiveClasses` and emits a duplicate-content
|
|
407
|
+
* Responsive preview block even though every breakpoint render is
|
|
408
|
+
* identical post-adapter.
|
|
409
|
+
*/
|
|
410
|
+
export function isShadcnAdapterDroppedTag(tagName: string): boolean {
|
|
411
|
+
return RADIX_SCROLL_AREA_RUNTIME_TAGS.has(tagName);
|
|
412
|
+
}
|
|
413
|
+
function flattenScrollAreaSubtree(node: NodeIR): NodeIR[] | null {
|
|
414
|
+
if (!('children' in node) || !Array.isArray(node.children)) return null;
|
|
415
|
+
let changed = false;
|
|
416
|
+
const next: NodeIR[] = [];
|
|
417
|
+
for (const child of node.children) {
|
|
418
|
+
if (!isClassedElement(child)) {
|
|
419
|
+
next.push(child);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const tag = (child as { tagName?: string }).tagName;
|
|
423
|
+
if (typeof tag === 'string' && RADIX_SCROLL_AREA_RUNTIME_TAGS.has(tag)) {
|
|
424
|
+
changed = true;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (tag === 'ScrollAreaPrimitive.Viewport') {
|
|
428
|
+
const viewportChildren = ('children' in child && Array.isArray(child.children))
|
|
429
|
+
? child.children
|
|
430
|
+
: [];
|
|
431
|
+
for (const vc of viewportChildren) next.push(vc);
|
|
432
|
+
changed = true;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
next.push(child);
|
|
436
|
+
}
|
|
437
|
+
return changed ? next : null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Recurse the IR, injecting registry classes for any element whose
|
|
442
|
+
* `data-slot` matches a known shadcn pattern AND applying shadcn-specific
|
|
443
|
+
* tree transforms (e.g. drop AvatarFallback when AvatarImage has a real
|
|
444
|
+
* src). Returns a new tree (input untouched) only when something
|
|
445
|
+
* actually changed; identical-shape input is returned by reference so
|
|
446
|
+
* downstream caches (`NODE_LAYOUT_CACHE`) stay warm.
|
|
447
|
+
*/
|
|
448
|
+
export function applyShadcnAdapter(node: NodeIR): NodeIR {
|
|
449
|
+
if (!node || node.kind === 'text' || node.kind === 'divider') return node;
|
|
450
|
+
|
|
451
|
+
if (node.kind === 'fragment') {
|
|
452
|
+
let changed = false;
|
|
453
|
+
const nextChildren: NodeIR[] = [];
|
|
454
|
+
for (const child of node.children) {
|
|
455
|
+
const next = applyShadcnAdapter(child);
|
|
456
|
+
if (next !== child) changed = true;
|
|
457
|
+
nextChildren.push(next);
|
|
458
|
+
}
|
|
459
|
+
return changed ? Object.assign({}, node, { children: nextChildren }) : node;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (node.kind === 'ring') {
|
|
463
|
+
const nextChild = applyShadcnAdapter(node.child);
|
|
464
|
+
return nextChild === node.child ? node : Object.assign({}, node, { child: nextChild });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let nextNode: NodeIR = node;
|
|
468
|
+
|
|
469
|
+
const slot = getDataSlot(node);
|
|
470
|
+
if (slot) {
|
|
471
|
+
const extras = SLOT_CLASS_INJECTIONS[slot];
|
|
472
|
+
if (extras && isClassedElement(node)) {
|
|
473
|
+
const merged = mergeMissing(node.classes ?? [], extras);
|
|
474
|
+
if (merged !== node.classes) {
|
|
475
|
+
nextNode = Object.assign({}, node, { classes: merged });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!('children' in nextNode) || !Array.isArray((nextNode as { children?: unknown }).children)) {
|
|
481
|
+
return nextNode;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Tree transform: shadcn Avatar drops its Fallback when the Image has
|
|
485
|
+
// a real src. Runs before recursion so the fallback subtree isn't
|
|
486
|
+
// walked unnecessarily.
|
|
487
|
+
if (isClassedElement(nextNode)) {
|
|
488
|
+
const filtered = dropFallbackWhenAvatarImageHasSrc(nextNode);
|
|
489
|
+
if (filtered) {
|
|
490
|
+
nextNode = Object.assign({}, nextNode, { children: filtered });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Tree transform: Radix ScrollArea drops its Scrollbar / Thumb /
|
|
495
|
+
// Corner overlays (runtime-only behaviour, not visual content) AND
|
|
496
|
+
// flattens its Viewport wrapper (no visual styling of its own —
|
|
497
|
+
// Figma's default frame fill would render it as a ghost-bordered
|
|
498
|
+
// duplicate of Root).
|
|
499
|
+
if (isClassedElement(nextNode)) {
|
|
500
|
+
const filtered = flattenScrollAreaSubtree(nextNode);
|
|
501
|
+
if (filtered) {
|
|
502
|
+
nextNode = Object.assign({}, nextNode, { children: filtered });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Tree transform: shadcn Progress reads `value` on Root and (at
|
|
507
|
+
// runtime) inline-styles the Indicator's transform/width. Inject a
|
|
508
|
+
// `w-[N%]` class on the descendant indicator so the universal
|
|
509
|
+
// fractional-width pipeline sizes it correctly.
|
|
510
|
+
if (isClassedElement(nextNode)) {
|
|
511
|
+
const withIndicator = applyProgressIndicatorWidth(nextNode);
|
|
512
|
+
if (withIndicator) {
|
|
513
|
+
nextNode = withIndicator;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Tree transform: shadcn Slider positions its indicator + thumbs from
|
|
518
|
+
// the Root's `defaultValue` / `value` prop at runtime. Inject percent
|
|
519
|
+
// positioning so each thumb sits at its value point and the indicator
|
|
520
|
+
// fills the value range. For range sliders the scanner only emits one
|
|
521
|
+
// thumb template — the transform clones it into N siblings.
|
|
522
|
+
if (isClassedElement(nextNode)) {
|
|
523
|
+
const sliderPositioned = applySliderPositioning(nextNode);
|
|
524
|
+
if (sliderPositioned) {
|
|
525
|
+
nextNode = sliderPositioned as typeof nextNode;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let childrenChanged = false;
|
|
530
|
+
const original = (nextNode as { children: NodeIR[] }).children;
|
|
531
|
+
const nextChildren: NodeIR[] = [];
|
|
532
|
+
for (const child of original) {
|
|
533
|
+
const next = applyShadcnAdapter(child);
|
|
534
|
+
if (next !== child) childrenChanged = true;
|
|
535
|
+
nextChildren.push(next);
|
|
536
|
+
}
|
|
537
|
+
if (childrenChanged) {
|
|
538
|
+
return Object.assign({}, nextNode, { children: nextChildren });
|
|
539
|
+
}
|
|
540
|
+
return nextNode;
|
|
541
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// All GitHub operations are relayed through the UI iframe because the
|
|
3
3
|
// Figma plugin sandbox has no direct network access.
|
|
4
4
|
|
|
5
|
-
import { loadConfig, GITHUB_CONFIG } from '
|
|
5
|
+
import { loadConfig, GITHUB_CONFIG, waitForUIReady } from '../plugin';
|
|
6
6
|
import {
|
|
7
7
|
TOKENS,
|
|
8
8
|
getVariableTokenDiffWithOptions,
|
|
@@ -11,10 +11,9 @@ import {
|
|
|
11
11
|
type ThemeTokens,
|
|
12
12
|
type TokenGroup,
|
|
13
13
|
type Tokens,
|
|
14
|
-
} from '
|
|
15
|
-
import { colorToLabel, debug, parseColor, mergeTokens as deepMergeTokens } from '
|
|
16
|
-
import {
|
|
17
|
-
import type { ResolvedTokenSourceMode, TokenSourceMode } from './token-source';
|
|
14
|
+
} from '../tokens';
|
|
15
|
+
import { colorToLabel, debug, parseColor, mergeTokens as deepMergeTokens } from '../tokens';
|
|
16
|
+
import type { ResolvedTokenSourceMode, TokenSourceMode } from '../tokens';
|
|
18
17
|
|
|
19
18
|
// ---------------------------------------------------------------------------
|
|
20
19
|
// Types
|
|
@@ -101,7 +100,7 @@ const CSS_DISCOVERY_PATHS = [
|
|
|
101
100
|
'styles/globals.css',
|
|
102
101
|
];
|
|
103
102
|
const PATCH_ENDPOINT_PORTS = [4000, 3000, 5173];
|
|
104
|
-
const PATCH_ENDPOINT_PATH = '/api/
|
|
103
|
+
const PATCH_ENDPOINT_PATH = '/api/inkbridge/patch-tokens';
|
|
105
104
|
|
|
106
105
|
// ---------------------------------------------------------------------------
|
|
107
106
|
// Module-level state for the GitHub fetch relay
|
|
@@ -398,16 +397,15 @@ export function applyClassChangesToFile(content: string, componentChange: Compon
|
|
|
398
397
|
}
|
|
399
398
|
|
|
400
399
|
function normalizeTokenSourceInfo(raw: unknown): TokenSourceInfo {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const modeRaw = raw && typeof raw === 'object' ? (raw as any).mode : null;
|
|
400
|
+
const obj = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : null;
|
|
401
|
+
const sourceRaw = obj && typeof obj.source === 'string' ? obj.source.trim() : '';
|
|
402
|
+
const source = sourceRaw || 'embedded:tokens.ts';
|
|
403
|
+
const modeRaw = obj ? obj.mode : null;
|
|
406
404
|
const mode: ResolvedTokenSourceMode =
|
|
407
405
|
modeRaw === 'css' || modeRaw === 'dtcg' || modeRaw === 'embedded' ? modeRaw : 'embedded';
|
|
408
|
-
const requestedRaw =
|
|
406
|
+
const requestedRaw = obj ? obj.requestedMode : null;
|
|
409
407
|
const requestedMode: TokenSourceMode | undefined =
|
|
410
|
-
requestedRaw === '
|
|
408
|
+
requestedRaw === 'css' || requestedRaw === 'dtcg' ? requestedRaw : undefined;
|
|
411
409
|
return { source, mode, requestedMode };
|
|
412
410
|
}
|
|
413
411
|
|
|
@@ -652,17 +650,40 @@ function getActiveThemeNames(tokens: Tokens): Set<string> {
|
|
|
652
650
|
return out;
|
|
653
651
|
}
|
|
654
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Detect whether the CSS file uses class-based theme selectors (.secondary)
|
|
655
|
+
* or data-theme attribute selectors (:root[data-theme="secondary"]).
|
|
656
|
+
* Defaults to 'class' (Tailwind-native) when no existing theme blocks are found.
|
|
657
|
+
*/
|
|
658
|
+
function detectThemeConvention(cssText: string): 'class' | 'data-theme' {
|
|
659
|
+
if (/\[data-theme\s*=\s*["']?[^"'\]]+["']?\]/.test(cssText)) return 'data-theme';
|
|
660
|
+
return 'class';
|
|
661
|
+
}
|
|
662
|
+
|
|
655
663
|
function parseRuleThemeNames(selector: string): string[] {
|
|
656
664
|
const themes = new Set<string>();
|
|
657
665
|
const normalized = String(selector || '').trim().toLowerCase();
|
|
658
666
|
if (!normalized) return [];
|
|
659
667
|
|
|
668
|
+
// data-theme attribute approach: :root[data-theme="secondary"]
|
|
660
669
|
const dataThemeMatches = normalized.matchAll(/\[data-theme\s*=\s*["']?([^"'\\\]]+)["']?\]/g);
|
|
661
670
|
for (const match of dataThemeMatches) {
|
|
662
671
|
const name = String(match[1] || '').trim();
|
|
663
672
|
if (name) themes.add(name);
|
|
664
673
|
}
|
|
665
674
|
|
|
675
|
+
// Class-based approach: .secondary, html.secondary
|
|
676
|
+
// Skip compound selectors that include .dark (e.g. .dark.secondary) — the
|
|
677
|
+
// plugin only writes light-theme tokens, so dark compound blocks are left alone.
|
|
678
|
+
const hasDarkClass = /(?:^|[\s.])dark\b/.test(normalized);
|
|
679
|
+
if (!hasDarkClass) {
|
|
680
|
+
const classMatches = normalized.matchAll(/\.([a-z][a-z0-9-]*)/g);
|
|
681
|
+
for (const match of classMatches) {
|
|
682
|
+
const name = String(match[1] || '').trim();
|
|
683
|
+
if (name && name !== 'dark' && name !== 'root') themes.add(name);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
666
687
|
if (/(\.|:)(dark)\b/.test(normalized)) themes.add('dark');
|
|
667
688
|
if (normalized.includes(':root') && themes.size === 0) themes.add('primary');
|
|
668
689
|
return Array.from(themes);
|
|
@@ -783,10 +804,10 @@ function findFirstBlockForTheme(cssText: string, themeName: string): CssRuleBloc
|
|
|
783
804
|
export function patchCssVariables(
|
|
784
805
|
cssText: string,
|
|
785
806
|
updatesByTheme: Map<string, Map<string, string>>,
|
|
786
|
-
activeThemeNames?: Set<string
|
|
787
|
-
,
|
|
807
|
+
activeThemeNames?: Set<string>,
|
|
788
808
|
fullThemeUpdatesByTheme?: Map<string, Map<string, string>>
|
|
789
809
|
): string {
|
|
810
|
+
const themeConvention = detectThemeConvention(cssText);
|
|
790
811
|
let nextCss = cssText;
|
|
791
812
|
const pending = cloneCssVariableUpdates(updatesByTheme);
|
|
792
813
|
const fullThemeUpdates = fullThemeUpdatesByTheme
|
|
@@ -843,7 +864,11 @@ export function patchCssVariables(
|
|
|
843
864
|
continue;
|
|
844
865
|
}
|
|
845
866
|
|
|
846
|
-
const selector = themeName === 'primary'
|
|
867
|
+
const selector = themeName === 'primary'
|
|
868
|
+
? ':root'
|
|
869
|
+
: themeConvention === 'class'
|
|
870
|
+
? '.' + themeName
|
|
871
|
+
: ':root[data-theme="' + themeName + '"]';
|
|
847
872
|
const completeThemeUpdates =
|
|
848
873
|
fullThemeUpdates && fullThemeUpdates.get(themeName)
|
|
849
874
|
? new Map(fullThemeUpdates.get(themeName) as Map<string, string>)
|
|
@@ -919,8 +944,8 @@ function queueFileIfChanged(
|
|
|
919
944
|
}
|
|
920
945
|
|
|
921
946
|
function resolveConfiguredMode(mode: unknown): TokenSourceMode {
|
|
922
|
-
if (mode === '
|
|
923
|
-
return '
|
|
947
|
+
if (mode === 'dtcg') return 'dtcg';
|
|
948
|
+
return 'css';
|
|
924
949
|
}
|
|
925
950
|
|
|
926
951
|
async function buildTokenCommitPlan(token: string): Promise<TokenCommitPlan> {
|
|
@@ -930,8 +955,8 @@ async function buildTokenCommitPlan(token: string): Promise<TokenCommitPlan> {
|
|
|
930
955
|
// This avoids rewriting untouched tokens and keeps push diffs focused.
|
|
931
956
|
const variableDiffOptions = {
|
|
932
957
|
allowNewTokensFromFigma: GITHUB_CONFIG!.allowNewTokensFromFigma === true,
|
|
933
|
-
newTokenPrefixes: Array.isArray(
|
|
934
|
-
?
|
|
958
|
+
newTokenPrefixes: Array.isArray(GITHUB_CONFIG!.newTokenPrefixes)
|
|
959
|
+
? GITHUB_CONFIG!.newTokenPrefixes
|
|
935
960
|
: [],
|
|
936
961
|
};
|
|
937
962
|
const variableTokenPatch = getVariableTokenDiffWithOptions(variableDiffOptions);
|
|
@@ -946,7 +971,7 @@ async function buildTokenCommitPlan(token: string): Promise<TokenCommitPlan> {
|
|
|
946
971
|
|
|
947
972
|
const preferDtcg =
|
|
948
973
|
configuredMode === 'dtcg' ||
|
|
949
|
-
(configuredMode === '
|
|
974
|
+
(configuredMode === 'css' && sourceInfo.mode === 'dtcg');
|
|
950
975
|
|
|
951
976
|
if (preferDtcg) {
|
|
952
977
|
const dtcg = tokensToDTCG(workingTokens);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './github';
|