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.
Files changed (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /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 './token-source';
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: 'auto',
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 === 'css' || mode === 'dtcg' || mode === 'auto') return mode;
47
- return 'auto';
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((storedConfig as any).allowNewTokensFromFigma),
93
- newTokenPrefixes: normalizeNewTokenPrefixes((storedConfig as any).newTokenPrefixes),
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 (e) {
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((config as any).tokenSourceMode);
109
- config.cssTokenPath = normalizeCssTokenPath((config as any).cssTokenPath);
110
- config.syncDtcgOnPush = normalizeSyncDtcgOnPush((config as any).syncDtcgOnPush);
111
- (config as any).allowNewTokensFromFigma = normalizeAllowNewTokensFromFigma((config as any).allowNewTokensFromFigma);
112
- (config as any).newTokenPrefixes = normalizeNewTokenPrefixes((config as any).newTokenPrefixes);
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 './colors';
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 = [...new Set(srcs.filter(Boolean))];
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 = (figma as any).createImage(new Uint8Array(bytes));
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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './config';
2
+ export * from './dev-server';
3
+ export * from '../plugin/packs';
@@ -0,0 +1,2 @@
1
+ export * from './packs';
2
+ export * from './pack-provider';
@@ -1,20 +1,20 @@
1
- import { debug } from './colors';
2
- import { waitForUIReady } from './dev-server';
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 './config';
5
+ import type { ProjectConfig } from '../../plugin';
6
6
 
7
7
  type PackFetchResult = {
8
8
  requestId: string;
9
- data: any | null;
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 = [4000, 3000, 5173];
17
- const PACK_PATHS = ['api/figma/scan-components'];
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((raw as any).components)) {
32
+ if (Array.isArray(raw.components)) {
33
33
  return raw;
34
34
  }
35
35
 
36
36
  // Pack envelope payload: { components: { ...componentDefs } }
37
- const nested = (raw as any).components;
38
- if (isPlainObject(nested) && Array.isArray((nested as any).components)) {
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 = (defs as any).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((defs as any).version);
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 === 'auto' || requestedMode === 'css' || requestedMode === 'dtcg') {
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 './tokens';
2
- import type { ComponentDefs } from './tokens';
3
- import { createEmptyScannedTokenMap, type ScannedTokenMap } from './token-source';
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?: any;
10
- instances?: any[];
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: any): ComponentDefs {
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, any>;
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 Record<string, any>;
53
- if (isPlainObject(raw.styleMap)) out.styleMap = raw.styleMap as Record<string, any>;
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: any): ScannedTokenMap | undefined {
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' || raw.requestedMode === 'auto'
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.themes)) out.themes = raw.themes as Record<string, any>;
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: any, fallbackId: string): Pack | null {
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: any = null;
84
- if (raw.components && Array.isArray(raw.components)) {
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((raw as any).tokens),
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,2 @@
1
+ // Auto-generated by build.mjs. Do not edit manually.
2
+ export const RENDER_ENGINE_VERSION = '011e89524e15bfda';
@@ -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 ComponentDefResolver = (name: string) => any | null;
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: any, tagName: string): string[] {
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: any = null;
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: any | null,
80
+ parentCompoundDef: ComponentDef | null,
54
81
  options: {
55
82
  getComponentDefByName: ComponentDefResolver;
56
- normalizeComponentDef: (def: any) => any;
83
+ normalizeComponentDef: (def: ComponentDef) => ComponentDef;
57
84
  hasWidthHintInClasses: (classes: string[]) => boolean;
58
- propsContainWidthHint: (props: Record<string, any> | undefined) => boolean;
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';