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,6 +1,6 @@
|
|
|
1
1
|
// Figma Plugin API types are provided by the runtime
|
|
2
2
|
|
|
3
|
-
import type { TokenSourceMode } from '
|
|
3
|
+
import type { TokenSourceMode } from '../tokens';
|
|
4
4
|
|
|
5
5
|
export interface ProjectConfig {
|
|
6
6
|
owner: string;
|
|
@@ -21,7 +21,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
|
|
|
21
21
|
repo: '',
|
|
22
22
|
baseBranch: 'main',
|
|
23
23
|
tokenPath: 'design-tokens/tokens.dtcg.json',
|
|
24
|
-
tokenSourceMode: '
|
|
24
|
+
tokenSourceMode: 'css',
|
|
25
25
|
cssTokenPath: '',
|
|
26
26
|
syncDtcgOnPush: false,
|
|
27
27
|
allowNewTokensFromFigma: false,
|
|
@@ -43,8 +43,8 @@ function normalizeTokenPath(tokenPath: unknown): string {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function normalizeTokenSourceMode(mode: unknown): TokenSourceMode {
|
|
46
|
-
if (mode === '
|
|
47
|
-
return '
|
|
46
|
+
if (mode === 'dtcg') return 'dtcg';
|
|
47
|
+
return 'css';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function normalizeCssTokenPath(cssTokenPath: unknown): string {
|
|
@@ -89,15 +89,15 @@ export async function loadConfig(): Promise<ProjectConfig> {
|
|
|
89
89
|
tokenSourceMode: normalizeTokenSourceMode(storedConfig.tokenSourceMode),
|
|
90
90
|
cssTokenPath: normalizeCssTokenPath(storedConfig.cssTokenPath),
|
|
91
91
|
syncDtcgOnPush: normalizeSyncDtcgOnPush(storedConfig.syncDtcgOnPush),
|
|
92
|
-
allowNewTokensFromFigma: normalizeAllowNewTokensFromFigma(
|
|
93
|
-
newTokenPrefixes: normalizeNewTokenPrefixes(
|
|
92
|
+
allowNewTokensFromFigma: normalizeAllowNewTokensFromFigma(storedConfig.allowNewTokensFromFigma),
|
|
93
|
+
newTokenPrefixes: normalizeNewTokenPrefixes(storedConfig.newTokenPrefixes),
|
|
94
94
|
};
|
|
95
95
|
// Persist one-time migrations so old installs stop carrying stale settings.
|
|
96
96
|
await figma.clientStorage.setAsync('project_config', GITHUB_CONFIG);
|
|
97
97
|
} else {
|
|
98
98
|
GITHUB_CONFIG = Object.assign({}, DEFAULT_CONFIG);
|
|
99
99
|
}
|
|
100
|
-
} catch (
|
|
100
|
+
} catch (_e) {
|
|
101
101
|
GITHUB_CONFIG = Object.assign({}, DEFAULT_CONFIG);
|
|
102
102
|
}
|
|
103
103
|
return GITHUB_CONFIG;
|
|
@@ -105,11 +105,11 @@ export async function loadConfig(): Promise<ProjectConfig> {
|
|
|
105
105
|
|
|
106
106
|
export async function saveConfig(config: ProjectConfig): Promise<void> {
|
|
107
107
|
config.tokenPath = normalizeTokenPath(config.tokenPath);
|
|
108
|
-
config.tokenSourceMode = normalizeTokenSourceMode(
|
|
109
|
-
config.cssTokenPath = normalizeCssTokenPath(
|
|
110
|
-
config.syncDtcgOnPush = normalizeSyncDtcgOnPush(
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
config.tokenSourceMode = normalizeTokenSourceMode(config.tokenSourceMode);
|
|
109
|
+
config.cssTokenPath = normalizeCssTokenPath(config.cssTokenPath);
|
|
110
|
+
config.syncDtcgOnPush = normalizeSyncDtcgOnPush(config.syncDtcgOnPush);
|
|
111
|
+
config.allowNewTokensFromFigma = normalizeAllowNewTokensFromFigma(config.allowNewTokensFromFigma);
|
|
112
|
+
config.newTokenPrefixes = normalizeNewTokenPrefixes(config.newTokenPrefixes);
|
|
113
113
|
GITHUB_CONFIG = config;
|
|
114
114
|
await figma.clientStorage.setAsync('project_config', config);
|
|
115
115
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 4. UI fetches from localhost dev server
|
|
8
8
|
// 5. UI sends 'component-defs-result' back to code.js
|
|
9
9
|
|
|
10
|
-
import { debug } from '
|
|
10
|
+
import { debug } from '../tokens';
|
|
11
11
|
|
|
12
12
|
interface ComponentDefsResult {
|
|
13
13
|
requestId: string;
|
|
@@ -123,7 +123,7 @@ export async function prefetchImages(
|
|
|
123
123
|
await waitForUIReady();
|
|
124
124
|
const hashMap = new Map<string, string>();
|
|
125
125
|
const svgMap = new Map<string, string>();
|
|
126
|
-
const unique =
|
|
126
|
+
const unique = Array.from(new Set(srcs.filter(Boolean)));
|
|
127
127
|
|
|
128
128
|
const entries = await Promise.all(
|
|
129
129
|
unique.map(async (src) => {
|
|
@@ -138,7 +138,7 @@ export async function prefetchImages(
|
|
|
138
138
|
svgMap.set(src, svgText);
|
|
139
139
|
} else if (bytes && bytes.length > 0) {
|
|
140
140
|
try {
|
|
141
|
-
const img =
|
|
141
|
+
const img = figma.createImage(new Uint8Array(bytes));
|
|
142
142
|
hashMap.set(src, img.hash);
|
|
143
143
|
} catch (_e) { /* ignore */ }
|
|
144
144
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ComponentDef } from '../components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Path/URL shapes the dev-server prefetch can resolve to a fetchable
|
|
5
|
+
* resource. Excludes data:/blob:/javascript: schemes (data: is handled by
|
|
6
|
+
* the browser directly; the others aren't useful in Figma).
|
|
7
|
+
*/
|
|
8
|
+
export function looksLikeImageSrc(src: string): boolean {
|
|
9
|
+
if (src.startsWith('/')) return true;
|
|
10
|
+
if (src.startsWith('http://') || src.startsWith('https://')) return true;
|
|
11
|
+
if (src.startsWith('./') || src.startsWith('../')) return true;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Walk a scanner-emitted JSX tree, capturing every `src` prop that points
|
|
17
|
+
* at a fetchable path/URL. Covers raw `<img>`, Next.js `<Image>`, AND
|
|
18
|
+
* shadcn-style image wrappers (`<AvatarImage>`, `<AvatarPrimitive.Image>`,
|
|
19
|
+
* any leaf component that forwards `src` to an underlying `<img>`).
|
|
20
|
+
*
|
|
21
|
+
* The wider net is required because `flattenComponentNodes` rewrites those
|
|
22
|
+
* compound primitives to `kind: 'element', tagLower: 'div'` with the `src`
|
|
23
|
+
* prop preserved — the renderer treats them as images, so the prefetch
|
|
24
|
+
* step must capture their srcs too. A tagName-only filter would silently
|
|
25
|
+
* skip every `<AvatarImage>` in user code.
|
|
26
|
+
*/
|
|
27
|
+
export function collectImageSrcsFromJsxTree(node: unknown, out: Set<string>): void {
|
|
28
|
+
if (!node || typeof node !== 'object') return;
|
|
29
|
+
const obj = node as Record<string, unknown>;
|
|
30
|
+
const props = obj.props && typeof obj.props === 'object' ? (obj.props as Record<string, unknown>) : null;
|
|
31
|
+
const src = props ? props.src : undefined;
|
|
32
|
+
if (typeof src === 'string' && src.length > 0 && src !== 'src' && looksLikeImageSrc(src)) {
|
|
33
|
+
out.add(src);
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(obj.children)) {
|
|
36
|
+
for (const child of obj.children) collectImageSrcsFromJsxTree(child, out);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function collectImageSrcs(components: ComponentDef[]): string[] {
|
|
41
|
+
const srcs = new Set<string>();
|
|
42
|
+
for (const comp of components) {
|
|
43
|
+
const analysis = comp?.analysis;
|
|
44
|
+
if (!analysis) continue;
|
|
45
|
+
collectImageSrcsFromJsxTree(analysis.jsxTree, srcs);
|
|
46
|
+
for (const story of analysis.stories ?? []) {
|
|
47
|
+
collectImageSrcsFromJsxTree(story.jsxTree, srcs);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// sandbox: spread isn't supported on Sets either; Array.from is safe.
|
|
51
|
+
return Array.from(srcs);
|
|
52
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { debug } from '
|
|
2
|
-
import { waitForUIReady } from '
|
|
1
|
+
import { debug } from '../../tokens';
|
|
2
|
+
import { waitForUIReady } from '../../plugin';
|
|
3
3
|
import { normalizePack } from './packs';
|
|
4
4
|
import type { Pack } from './packs';
|
|
5
|
-
import type { ProjectConfig } from '
|
|
5
|
+
import type { ProjectConfig } from '../../plugin';
|
|
6
6
|
|
|
7
7
|
type PackFetchResult = {
|
|
8
8
|
requestId: string;
|
|
9
|
-
data:
|
|
9
|
+
data: unknown | null;
|
|
10
10
|
source?: string;
|
|
11
11
|
error?: string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
type PackFetchConfig = Pick<ProjectConfig, 'tokenPath' | 'tokenSourceMode' | 'cssTokenPath'>;
|
|
15
15
|
|
|
16
|
-
const PACK_PORTS = [
|
|
17
|
-
const PACK_PATHS = ['api/
|
|
16
|
+
const PACK_PORTS = [3000, 4000, 5173];
|
|
17
|
+
const PACK_PATHS = ['api/inkbridge/scan-components'];
|
|
18
18
|
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
19
19
|
const SUPPORTED_COMPONENT_DEFS_MAJOR = 1;
|
|
20
20
|
|
|
@@ -29,13 +29,13 @@ function getComponentDefsPayload(raw: unknown): Record<string, unknown> | null {
|
|
|
29
29
|
if (!isPlainObject(raw)) return null;
|
|
30
30
|
|
|
31
31
|
// Direct component-definitions payload
|
|
32
|
-
if (Array.isArray(
|
|
32
|
+
if (Array.isArray(raw.components)) {
|
|
33
33
|
return raw;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Pack envelope payload: { components: { ...componentDefs } }
|
|
37
|
-
const nested =
|
|
38
|
-
if (isPlainObject(nested) && Array.isArray(
|
|
37
|
+
const nested = raw.components;
|
|
38
|
+
if (isPlainObject(nested) && Array.isArray(nested.components)) {
|
|
39
39
|
return nested;
|
|
40
40
|
}
|
|
41
41
|
return null;
|
|
@@ -53,7 +53,7 @@ function validateComponentDefsContract(raw: unknown): { ok: true } | { ok: false
|
|
|
53
53
|
const defs = getComponentDefsPayload(raw);
|
|
54
54
|
if (!defs) return { ok: false, error: 'invalid-pack' };
|
|
55
55
|
|
|
56
|
-
const schemaVersion =
|
|
56
|
+
const schemaVersion = defs.schemaVersion;
|
|
57
57
|
if (typeof schemaVersion === 'number') {
|
|
58
58
|
if (schemaVersion !== SUPPORTED_SCHEMA_VERSION) {
|
|
59
59
|
return { ok: false, error: 'incompatible-schema-version' };
|
|
@@ -62,7 +62,7 @@ function validateComponentDefsContract(raw: unknown): { ok: true } | { ok: false
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Backward compatibility with older payloads that only had `version`.
|
|
65
|
-
const major = parseMajor(
|
|
65
|
+
const major = parseMajor(defs.version);
|
|
66
66
|
if (major != null && major !== SUPPORTED_COMPONENT_DEFS_MAJOR) {
|
|
67
67
|
return { ok: false, error: 'incompatible-pack-version' };
|
|
68
68
|
}
|
|
@@ -79,7 +79,7 @@ function buildPackCandidates(config?: PackFetchConfig): string[] {
|
|
|
79
79
|
const baseUrl = 'http://localhost:' + port + '/' + path;
|
|
80
80
|
const query: string[] = [];
|
|
81
81
|
const requestedMode = config?.tokenSourceMode;
|
|
82
|
-
if (requestedMode === '
|
|
82
|
+
if (requestedMode === 'css' || requestedMode === 'dtcg') {
|
|
83
83
|
query.push('tokenSourceMode=' + encodeURIComponent(requestedMode));
|
|
84
84
|
}
|
|
85
85
|
const cssTokenPath = (config?.cssTokenPath || '').trim();
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { COMPONENT_DEFS } from '
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { COMPONENT_DEFS } from '../../tokens';
|
|
2
|
+
import { resetComponentLookupCaches } from '../../components';
|
|
3
|
+
import type { ComponentDefs } from '../../tokens';
|
|
4
|
+
import type { ComponentInstance } from '../../components';
|
|
5
|
+
import { createEmptyScannedTokenMap, type ScannedTokenMap } from '../../tokens';
|
|
6
|
+
import type { JsxNode } from '../../tailwind';
|
|
4
7
|
|
|
5
8
|
export type StoryDefinition = {
|
|
6
9
|
id: string;
|
|
7
10
|
name: string;
|
|
8
11
|
layoutClasses?: string[] | string;
|
|
9
|
-
jsxTree?:
|
|
10
|
-
instances?:
|
|
12
|
+
jsxTree?: JsxNode;
|
|
13
|
+
instances?: ComponentInstance[];
|
|
11
14
|
tags?: string[];
|
|
12
15
|
};
|
|
13
16
|
|
|
@@ -27,7 +30,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
27
30
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
function normalizeComponentDefs(raw:
|
|
33
|
+
function normalizeComponentDefs(raw: unknown): ComponentDefs {
|
|
31
34
|
const out: ComponentDefs = {
|
|
32
35
|
schemaVersion: undefined,
|
|
33
36
|
version: undefined,
|
|
@@ -46,20 +49,20 @@ function normalizeComponentDefs(raw: any): ComponentDefs {
|
|
|
46
49
|
if (typeof raw.version === 'string') out.version = raw.version;
|
|
47
50
|
if (typeof raw.generatedAt === 'string') out.generatedAt = raw.generatedAt;
|
|
48
51
|
if (Array.isArray(raw.components)) out.components = raw.components;
|
|
49
|
-
if (isPlainObject(raw.spacingScale)) out.spacingScale = raw.spacingScale as Record<string,
|
|
52
|
+
if (isPlainObject(raw.spacingScale)) out.spacingScale = raw.spacingScale as Record<string, number>;
|
|
50
53
|
if (Array.isArray(raw.colorTokens)) out.colorTokens = raw.colorTokens;
|
|
51
54
|
if (isPlainObject(raw.paletteTokens)) out.paletteTokens = raw.paletteTokens as Record<string, string>;
|
|
52
|
-
if (isPlainObject(raw.iconRegistry)) out.iconRegistry = raw.iconRegistry as
|
|
53
|
-
if (isPlainObject(raw.styleMap)) out.styleMap = raw.styleMap as
|
|
55
|
+
if (isPlainObject(raw.iconRegistry)) out.iconRegistry = raw.iconRegistry as ComponentDefs['iconRegistry'];
|
|
56
|
+
if (isPlainObject(raw.styleMap)) out.styleMap = raw.styleMap as ComponentDefs['styleMap'];
|
|
54
57
|
return out;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
function normalizeTokenMap(raw:
|
|
60
|
+
function normalizeTokenMap(raw: unknown): ScannedTokenMap | undefined {
|
|
58
61
|
if (!raw || !isPlainObject(raw)) return undefined;
|
|
59
62
|
const mode = raw.mode === 'css' || raw.mode === 'dtcg' || raw.mode === 'embedded'
|
|
60
63
|
? raw.mode
|
|
61
64
|
: 'embedded';
|
|
62
|
-
const requestedMode = raw.requestedMode === 'css' || raw.requestedMode === 'dtcg'
|
|
65
|
+
const requestedMode = raw.requestedMode === 'css' || raw.requestedMode === 'dtcg'
|
|
63
66
|
? raw.requestedMode
|
|
64
67
|
: undefined;
|
|
65
68
|
const out = createEmptyScannedTokenMap(
|
|
@@ -73,15 +76,16 @@ function normalizeTokenMap(raw: any): ScannedTokenMap | undefined {
|
|
|
73
76
|
if (isPlainObject(raw.spacing)) out.spacing = raw.spacing as Record<string, number>;
|
|
74
77
|
if (isPlainObject(raw.fontSize)) out.fontSize = raw.fontSize as Record<string, number>;
|
|
75
78
|
if (isPlainObject(raw.shadows)) out.shadows = raw.shadows as Record<string, string>;
|
|
76
|
-
if (isPlainObject(raw.
|
|
79
|
+
if (isPlainObject(raw.breakpoints)) out.breakpoints = raw.breakpoints as Record<string, number>;
|
|
80
|
+
if (isPlainObject(raw.themes)) out.themes = raw.themes as ScannedTokenMap['themes'];
|
|
77
81
|
return out;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
export function normalizePack(raw:
|
|
81
|
-
if (!raw) return null;
|
|
84
|
+
export function normalizePack(raw: unknown, fallbackId: string): Pack | null {
|
|
85
|
+
if (!raw || !isPlainObject(raw)) return null;
|
|
82
86
|
|
|
83
|
-
let componentDefs:
|
|
84
|
-
if (
|
|
87
|
+
let componentDefs: unknown = null;
|
|
88
|
+
if (Array.isArray(raw.components)) {
|
|
85
89
|
componentDefs = raw;
|
|
86
90
|
} else if (raw.components && isPlainObject(raw.components) && Array.isArray(raw.components.components)) {
|
|
87
91
|
componentDefs = raw.components;
|
|
@@ -97,7 +101,7 @@ export function normalizePack(raw: any, fallbackId: string): Pack | null {
|
|
|
97
101
|
name: packName,
|
|
98
102
|
version: typeof raw.version === 'string' ? raw.version : undefined,
|
|
99
103
|
components: normalizeComponentDefs(componentDefs),
|
|
100
|
-
tokens: normalizeTokenMap(
|
|
104
|
+
tokens: normalizeTokenMap(raw.tokens),
|
|
101
105
|
stories: Array.isArray(raw.stories) ? raw.stories : undefined,
|
|
102
106
|
tags: Array.isArray(raw.tags) ? raw.tags : undefined,
|
|
103
107
|
};
|
|
@@ -115,6 +119,7 @@ export function applyPack(pack: Pack): void {
|
|
|
115
119
|
COMPONENT_DEFS.paletteTokens = defs.paletteTokens;
|
|
116
120
|
COMPONENT_DEFS.iconRegistry = defs.iconRegistry;
|
|
117
121
|
COMPONENT_DEFS.styleMap = defs.styleMap;
|
|
122
|
+
resetComponentLookupCaches();
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
export function getActivePack(): Pack | null {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for tree-transform "adapters" — both framework-specific
|
|
3
|
+
* (shadcn / Radix data-slot patches in `framework-adapters/shadcn.ts`) and
|
|
4
|
+
* native-HTML rewrites (e.g. `<input type="range">` in `node-ir.ts`'s
|
|
5
|
+
* transform chain).
|
|
6
|
+
*
|
|
7
|
+
* Why a separate file
|
|
8
|
+
* -------------------
|
|
9
|
+
* `framework-adapters/shadcn.ts` and `tailwind/node-ir.ts` already have a
|
|
10
|
+
* one-way `node-ir → framework-adapters` dependency for the dispatcher.
|
|
11
|
+
* Putting these helpers in either would either deepen that cycle (shadcn →
|
|
12
|
+
* node-ir for `isClassedElement`) or hide them in a "framework adapter"
|
|
13
|
+
* module when they aren't framework-specific. A sibling utility module
|
|
14
|
+
* keeps the import graph clean: both sides import down into this file.
|
|
15
|
+
*
|
|
16
|
+
* All helpers must be sandbox-safe — no value-side spread, no DOM, no
|
|
17
|
+
* external I/O — these run inside the Figma plugin's code.js.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { NodeIR } from './node-ir';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Type guard for nodes that carry `props` + `classes`. `text`, `fragment`,
|
|
24
|
+
* `divider`, and `ring` nodes do not; `ring` wraps a single child via a
|
|
25
|
+
* separate field. Tree-transform code typically branches first on this.
|
|
26
|
+
*/
|
|
27
|
+
export function isClassedElement(
|
|
28
|
+
node: NodeIR,
|
|
29
|
+
): node is Extract<NodeIR, { kind: 'element' | 'component' }> {
|
|
30
|
+
return node.kind === 'element' || node.kind === 'component';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Additive class merge: append `extras` to `existing` only when not
|
|
35
|
+
* already present. Preserves order. Returns `existing` by reference when
|
|
36
|
+
* nothing was added so callers can detect "no change" and keep object
|
|
37
|
+
* identity stable (downstream caches like `NODE_LAYOUT_CACHE` rely on
|
|
38
|
+
* this).
|
|
39
|
+
*
|
|
40
|
+
* Note: this is the "injection" merge, distinct from `class-utils.ts`'s
|
|
41
|
+
* `mergeClasses` which implements Tailwind override semantics
|
|
42
|
+
* (last-wins). Use this when an adapter wants to ADD classes without
|
|
43
|
+
* disturbing the consumer's own class order.
|
|
44
|
+
*/
|
|
45
|
+
export function mergeMissing(existing: string[], extras: readonly string[]): string[] {
|
|
46
|
+
if (extras.length === 0) return existing;
|
|
47
|
+
const set = new Set(existing);
|
|
48
|
+
let out: string[] | null = null;
|
|
49
|
+
for (const cls of extras) {
|
|
50
|
+
if (set.has(cls)) continue;
|
|
51
|
+
if (!out) out = existing.slice();
|
|
52
|
+
out.push(cls);
|
|
53
|
+
set.add(cls);
|
|
54
|
+
}
|
|
55
|
+
return out ?? existing;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a value/min/max trio into an array of percent positions
|
|
60
|
+
* (0-100). The scanner stringifies every JSX prop, so `<input value={5}>`
|
|
61
|
+
* arrives as `props.value === "5"`. We accept numbers, strings,
|
|
62
|
+
* JSON-array strings ("[25, 75]" for shadcn range sliders), and an actual
|
|
63
|
+
* array of numbers/strings.
|
|
64
|
+
*
|
|
65
|
+
* Defaults — when `min` or `max` is missing / unparseable:
|
|
66
|
+
* - `min` defaults to 0
|
|
67
|
+
* - `max` defaults to 100
|
|
68
|
+
* - If `max <= min` after parsing (degenerate range), we fall back to
|
|
69
|
+
* `min + 100` so the percent math doesn't divide by zero.
|
|
70
|
+
*
|
|
71
|
+
* If `value` can't be parsed at all, returns `[0]` — a recognisable
|
|
72
|
+
* "left-edge" position. Callers that want a different default (e.g. HTML
|
|
73
|
+
* `<input type=range>`'s `(min+max)/2`) can post-process.
|
|
74
|
+
*
|
|
75
|
+
* Used by:
|
|
76
|
+
* - shadcn Slider adapter (`applySliderPositioning`) — passes min=0
|
|
77
|
+
* unconditionally because base-ui's Slider is min=0 max=`max` only.
|
|
78
|
+
* - `<input type="range">` IR transform — passes the actual min from
|
|
79
|
+
* the element's prop (HTML default min is 0 but consumers commonly
|
|
80
|
+
* override).
|
|
81
|
+
*/
|
|
82
|
+
export function resolveValuePercents(
|
|
83
|
+
rawValue: unknown,
|
|
84
|
+
rawMin: unknown,
|
|
85
|
+
rawMax: unknown,
|
|
86
|
+
): number[] {
|
|
87
|
+
const min = parseNumeric(rawMin, 0);
|
|
88
|
+
let max = parseNumeric(rawMax, 100);
|
|
89
|
+
if (!(max > min)) max = min + 100;
|
|
90
|
+
const range = max - min;
|
|
91
|
+
const toPct = (v: number): number =>
|
|
92
|
+
Math.max(0, Math.min(100, ((v - min) / range) * 100));
|
|
93
|
+
|
|
94
|
+
if (Array.isArray(rawValue)) {
|
|
95
|
+
const nums: number[] = [];
|
|
96
|
+
for (const v of rawValue) {
|
|
97
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' ? parseFloat(v) : NaN;
|
|
98
|
+
if (Number.isFinite(n)) nums.push(toPct(n));
|
|
99
|
+
}
|
|
100
|
+
if (nums.length > 0) return nums;
|
|
101
|
+
}
|
|
102
|
+
if (typeof rawValue === 'number' && Number.isFinite(rawValue)) {
|
|
103
|
+
return [toPct(rawValue)];
|
|
104
|
+
}
|
|
105
|
+
if (typeof rawValue === 'string') {
|
|
106
|
+
const trimmed = rawValue.trim();
|
|
107
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(trimmed);
|
|
110
|
+
if (Array.isArray(parsed)) {
|
|
111
|
+
const nums: number[] = [];
|
|
112
|
+
for (const v of parsed) {
|
|
113
|
+
const n = typeof v === 'number' ? v : parseFloat(String(v));
|
|
114
|
+
if (Number.isFinite(n)) nums.push(toPct(n));
|
|
115
|
+
}
|
|
116
|
+
if (nums.length > 0) return nums;
|
|
117
|
+
}
|
|
118
|
+
} catch (_e) {
|
|
119
|
+
// Not valid JSON — fall through to numeric parse.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const num = parseFloat(trimmed);
|
|
123
|
+
if (Number.isFinite(num)) return [toPct(num)];
|
|
124
|
+
}
|
|
125
|
+
return [0];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseNumeric(raw: unknown, fallback: number): number {
|
|
129
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
|
|
130
|
+
if (typeof raw === 'string') {
|
|
131
|
+
const trimmed = raw.trim();
|
|
132
|
+
if (trimmed.length === 0) return fallback;
|
|
133
|
+
const n = parseFloat(trimmed);
|
|
134
|
+
if (Number.isFinite(n)) return n;
|
|
135
|
+
}
|
|
136
|
+
return fallback;
|
|
137
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { type JsxNode, type JsxElement, splitClassName } from './node-ir';
|
|
2
|
+
import type { ComponentDef } from '../components/scanner-types';
|
|
2
3
|
|
|
3
|
-
export type
|
|
4
|
+
export type { ComponentDef };
|
|
5
|
+
export type ComponentDefResolver = (name: string) => ComponentDef | null;
|
|
6
|
+
|
|
7
|
+
// Returns the class as-is if it has no variant prefix (e.g. `hover:`, `md:`),
|
|
8
|
+
// or null if any variant is present. Use this when only the unprefixed default
|
|
9
|
+
// class should drive a layout/styling decision. To strip variants and keep the
|
|
10
|
+
// base, see `effects/icon-builder.ts` which intentionally takes the inner.
|
|
11
|
+
export function getBaseClass(value: string): string | null {
|
|
12
|
+
if (!value) return null;
|
|
13
|
+
if (value.indexOf(':') !== -1) return null;
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
export function mergeClasses(base: string[], extra: string[]): string[] {
|
|
6
18
|
const seen: Record<string, boolean> = {};
|
|
@@ -30,10 +42,25 @@ export function mergeClasses(base: string[], extra: string[]): string[] {
|
|
|
30
42
|
return out;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
export function getCompoundClasses(def:
|
|
45
|
+
export function getCompoundClasses(def: ComponentDef, tagName: string): string[] {
|
|
34
46
|
if (!def || def.type !== 'compound' || !def.subComponents) return [];
|
|
47
|
+
// Strict lowercase match. We previously tried using the same elaborate
|
|
48
|
+
// normalization as `getComponentDefByName` here (so
|
|
49
|
+
// `<AvatarPrimitive.Image>` would resolve to the `AvatarImage`
|
|
50
|
+
// subComponent and get flattened to a div), but that side-effect
|
|
51
|
+
// also flattened portal primitives like `<DialogPrimitive.Trigger>`
|
|
52
|
+
// / `<SheetPrimitive.Content>` / `<SelectPrimitive.Content>` /
|
|
53
|
+
// `<AccordionPrimitive.Content>`, which need to stay `kind: 'component'`
|
|
54
|
+
// so the portal-aware rendering path can position them as overlays.
|
|
55
|
+
// Net effect: Dialog/Sheet/Select/Accordion-open/Mobile-Nav rendered
|
|
56
|
+
// their portal content as inline siblings of the trigger.
|
|
57
|
+
//
|
|
58
|
+
// AvatarPrimitive.Image is unaffected because the image branch in
|
|
59
|
+
// ui-builder accepts both `kind: 'element'` and `kind: 'component'`
|
|
60
|
+
// via `isLeafImageLike` — the strict lookup leaves it as `'component'`
|
|
61
|
+
// but the image branch still picks it up by the `src` prop.
|
|
35
62
|
const target = tagName.toLowerCase();
|
|
36
|
-
let fallback:
|
|
63
|
+
let fallback: ComponentDef = null;
|
|
37
64
|
for (const sub of def.subComponents) {
|
|
38
65
|
if (!sub || !sub.name) continue;
|
|
39
66
|
const subName = String(sub.name).toLowerCase();
|
|
@@ -50,12 +77,12 @@ export function getCompoundClasses(def: any, tagName: string): string[] {
|
|
|
50
77
|
|
|
51
78
|
export function treeHasFullWidth(
|
|
52
79
|
node: JsxNode | undefined,
|
|
53
|
-
parentCompoundDef:
|
|
80
|
+
parentCompoundDef: ComponentDef | null,
|
|
54
81
|
options: {
|
|
55
82
|
getComponentDefByName: ComponentDefResolver;
|
|
56
|
-
normalizeComponentDef: (def:
|
|
83
|
+
normalizeComponentDef: (def: ComponentDef) => ComponentDef;
|
|
57
84
|
hasWidthHintInClasses: (classes: string[]) => boolean;
|
|
58
|
-
propsContainWidthHint: (props: Record<string,
|
|
85
|
+
propsContainWidthHint: (props: Record<string, unknown> | undefined) => boolean;
|
|
59
86
|
}
|
|
60
87
|
): boolean {
|
|
61
88
|
if (!node || node.type !== 'element') return false;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './tailwind';
|
|
2
|
+
export * from './utility-resolver';
|
|
3
|
+
export * from './class-utils';
|
|
4
|
+
export * from './jsx-utils';
|
|
5
|
+
export * from './node-ir';
|
|
6
|
+
export * from './responsive-analyzer';
|
|
7
|
+
export * from './state-analyzer';
|
|
8
|
+
export * from './transform-math';
|