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
@@ -0,0 +1,161 @@
1
+ import { COMPONENT_DEFS } from '../tokens';
2
+ import type { ComponentAnalysis, EnrichedComponentAnalysis } from '../../scanner/types';
3
+
4
+ type RawComponentEntry = EnrichedComponentAnalysis | ComponentAnalysis;
5
+
6
+ export type IconRegistryEntry = {
7
+ module: string;
8
+ exportName: string;
9
+ svg: string;
10
+ };
11
+
12
+ const REACT_ICON_MAP: Record<string, string> = {
13
+ HiOutlineHome: 'home',
14
+ HiHome: 'home',
15
+ RiTableView: 'portfolio',
16
+ HiMiniSquares2x2: 'portfolio',
17
+ RiCompass3Line: 'strategy',
18
+ RiWallet2Fill: 'pda',
19
+ RiAccountBoxLine: 'account',
20
+ HiLogout: 'logout',
21
+ RiLogoutBoxLine: 'logout',
22
+ HiMenu: 'menu',
23
+ HiX: 'close',
24
+ HiCheck: 'check',
25
+ ChevronDown: 'chevron-down',
26
+ ChevronDownIcon: 'chevron-down',
27
+ };
28
+
29
+ let COMPONENT_DEF_MAP: Record<string, RawComponentEntry> | null = null;
30
+ let ICON_REGISTRY_MAP: Record<string, IconRegistryEntry> | null = null;
31
+
32
+ export function resetComponentLookupCaches(): void {
33
+ COMPONENT_DEF_MAP = null;
34
+ ICON_REGISTRY_MAP = null;
35
+ }
36
+
37
+ function normalizeLookupKey(value: string): string {
38
+ return String(value || '').toLowerCase().trim();
39
+ }
40
+
41
+ function compactLookupKey(value: string): string {
42
+ return normalizeLookupKey(value).replace(/[^a-z0-9]/g, '');
43
+ }
44
+
45
+ function addLookupKey(map: Record<string, RawComponentEntry>, key: string, value: RawComponentEntry): void {
46
+ const normalized = normalizeLookupKey(key);
47
+ if (normalized && !map[normalized]) {
48
+ map[normalized] = value;
49
+ }
50
+ const compact = compactLookupKey(key);
51
+ if (compact && !map[compact]) {
52
+ map[compact] = value;
53
+ }
54
+ }
55
+
56
+ export function getComponentLookupCandidates(name: string): string[] {
57
+ return getLookupCandidates(name);
58
+ }
59
+
60
+ function getLookupCandidates(name: string): string[] {
61
+ const raw = normalizeLookupKey(name);
62
+ const out: string[] = [];
63
+ const seen: Record<string, true> = {};
64
+ const push = (candidate: string): void => {
65
+ const normalized = normalizeLookupKey(candidate);
66
+ if (!normalized || seen[normalized]) return;
67
+ seen[normalized] = true;
68
+ out.push(normalized);
69
+ };
70
+
71
+ push(raw);
72
+ push(compactLookupKey(raw));
73
+
74
+ const noRoot = raw.replace(/([._/\-])?root$/, '');
75
+ if (noRoot !== raw) {
76
+ push(noRoot);
77
+ push(compactLookupKey(noRoot));
78
+ const withoutPrimitive = noRoot.replace(/([._/\-])?primitive$/, '');
79
+ if (withoutPrimitive !== noRoot) {
80
+ push(withoutPrimitive);
81
+ push(compactLookupKey(withoutPrimitive));
82
+ }
83
+ }
84
+
85
+ const compact = compactLookupKey(raw);
86
+ if (compact.endsWith('root')) {
87
+ const compactNoRoot = compact.slice(0, -4);
88
+ push(compactNoRoot);
89
+ if (compactNoRoot.endsWith('primitive')) {
90
+ push(compactNoRoot.slice(0, -9));
91
+ }
92
+ }
93
+
94
+ if (compact.includes('primitive')) {
95
+ const compactNoPrimitive = compact.replace(/primitive/g, '');
96
+ push(compactNoPrimitive);
97
+ if (compactNoPrimitive.endsWith('root')) {
98
+ push(compactNoPrimitive.slice(0, -4));
99
+ }
100
+ }
101
+
102
+ return out;
103
+ }
104
+
105
+ export function getComponentDefByName(name: string): RawComponentEntry | null {
106
+ const defName = String(name || '');
107
+ if (!COMPONENT_DEF_MAP) {
108
+ COMPONENT_DEF_MAP = {};
109
+ const defsArray = ((COMPONENT_DEFS && COMPONENT_DEFS.components) ? COMPONENT_DEFS.components : []) as RawComponentEntry[];
110
+ for (let i = 0; i < defsArray.length; i++) {
111
+ const d = defsArray[i];
112
+ const analysis = 'analysis' in d ? d.analysis : d;
113
+ if (!analysis || !analysis.name) continue;
114
+ const key = String(analysis.name);
115
+ addLookupKey(COMPONENT_DEF_MAP, key, d);
116
+ // Also index under dash-stripped key so PascalCase JSX refs (e.g. "HeroSection")
117
+ // resolve to kebab-case scanner names (e.g. "Hero-section" → "herosection").
118
+ const dashlessKey = normalizeLookupKey(key).replace(/-/g, '');
119
+ addLookupKey(COMPONENT_DEF_MAP, dashlessKey, d);
120
+ }
121
+ }
122
+ const candidates = getLookupCandidates(defName);
123
+ for (let i = 0; i < candidates.length; i++) {
124
+ const candidate = candidates[i];
125
+ if (COMPONENT_DEF_MAP[candidate]) {
126
+ return COMPONENT_DEF_MAP[candidate];
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function getIconRegistry(): Record<string, IconRegistryEntry> {
133
+ if (ICON_REGISTRY_MAP) return ICON_REGISTRY_MAP;
134
+ const registry = (COMPONENT_DEFS && COMPONENT_DEFS.iconRegistry) ? COMPONENT_DEFS.iconRegistry : {};
135
+ ICON_REGISTRY_MAP = {};
136
+ for (const key in registry) {
137
+ ICON_REGISTRY_MAP[key] = registry[key];
138
+ }
139
+ return ICON_REGISTRY_MAP;
140
+ }
141
+
142
+ export function getIconRegistryEntry(name: string): IconRegistryEntry | null {
143
+ if (!name) return null;
144
+ const registry = getIconRegistry();
145
+ if (registry[name]) return registry[name];
146
+ const target = name.toLowerCase();
147
+ for (const key in registry) {
148
+ if (key.toLowerCase() === target) return registry[key];
149
+ }
150
+ return null;
151
+ }
152
+
153
+ export function getReactIconKey(name: string): string | null {
154
+ if (!name) return null;
155
+ if (REACT_ICON_MAP[name]) return REACT_ICON_MAP[name];
156
+ const target = name.toLowerCase();
157
+ for (const key in REACT_ICON_MAP) {
158
+ if (key.toLowerCase() === target) return REACT_ICON_MAP[key];
159
+ }
160
+ return null;
161
+ }
@@ -0,0 +1,7 @@
1
+ export * from './component-defs';
2
+ export * from './component-gen';
3
+ export * from './component-instance';
4
+ export * from './component-library';
5
+ export * from './component-lookup';
6
+ export * from './scanner-types';
7
+ export * from './symbol-instance-policy';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Type aliases for the unstructured JSON shapes that flow from the
3
+ * scanner (tools/figma-plugin/scanner/) into the plugin runtime.
4
+ *
5
+ * These shapes have grown organically over many iterations and don't
6
+ * yet have a single canonical schema (the scanner emits one JSON blob,
7
+ * the plugin consumes parts of it from many call sites). Until the
8
+ * schema is formalized, these aliases give the consumers a *named*
9
+ * `any` so the call sites read intentionally — `def: ComponentDef`
10
+ * makes it obvious the parameter is a scanner-emitted def — rather
11
+ * than scattering bare `any` everywhere.
12
+ *
13
+ * Each alias is the only place `any` appears for that shape; consumers
14
+ * import the name and never see the literal `any`.
15
+ */
16
+
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ export type ComponentDef = any;
19
+
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ export type ComponentInstance = any;
22
+
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ export type ComponentStory = any;
25
+
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ export type CvaAnalysis = any;
28
+
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ export type LayoutInfo = any;
31
+
32
+ /**
33
+ * Type-erasure boundary used by InstanceBackend to accept differently-shaped
34
+ * ctx objects from each consumer (story-builder vs ui-builder). Each consumer
35
+ * passes its own typed ctx through to the backend's helper hooks; the backend
36
+ * itself doesn't reach into the shape.
37
+ */
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ export type BackendCtx = any;
@@ -0,0 +1,312 @@
1
+ import { splitClassName } from '../tailwind';
2
+ import type { ComponentDef, ComponentInstance } from './scanner-types';
3
+
4
+ const IGNORED_SYMBOL_PROP_KEYS = new Set([
5
+ 'className',
6
+ 'children',
7
+ 'placeholder',
8
+ 'defaultValue',
9
+ 'aria-invalid',
10
+ 'disabled',
11
+ 'ref',
12
+ 'key',
13
+ ]);
14
+
15
+ export type SymbolPolicyAnalysis = {
16
+ reason: string | null;
17
+ ignoredProps: string[];
18
+ slotPropMappings: Record<string, string>;
19
+ };
20
+
21
+ type TextPropInferenceStats = {
22
+ stringCount: number;
23
+ nonStringCount: number;
24
+ uniqueValues: Record<string, boolean>;
25
+ hasLikelyTextValue: boolean;
26
+ hasUrlLikeValue: boolean;
27
+ hasTokenLikeValue: boolean;
28
+ };
29
+
30
+ const SAFE_TEXT_PROP_CACHE: Record<string, string[] | undefined> = {};
31
+
32
+ function pushUnique(out: string[], value: string): void {
33
+ if (!value) return;
34
+ if (out.indexOf(value) !== -1) return;
35
+ out.push(value);
36
+ }
37
+
38
+ export function isEventHandlerPropKey(key: string): boolean {
39
+ if (!key || key.length < 3) return false;
40
+ if (!(key[0] === 'o' && key[1] === 'n')) return false;
41
+ const firstSuffixChar = key[2];
42
+ return firstSuffixChar === firstSuffixChar.toUpperCase() && firstSuffixChar !== firstSuffixChar.toLowerCase();
43
+ }
44
+
45
+ export function isNonVisualA11yPropKey(key: string): boolean {
46
+ return key === 'aria-label'
47
+ || key === 'aria-labelledby'
48
+ || key === 'aria-describedby'
49
+ || key === 'aria-controls'
50
+ || key === 'aria-details';
51
+ }
52
+
53
+ export function isNonVisualMetaPropKey(key: string): boolean {
54
+ if (key === 'id' || key === 'role' || key === 'tabIndex') return true;
55
+ if (key === 'data-testid' || key === 'data-cy') return true;
56
+ return key.indexOf('data-test') === 0;
57
+ }
58
+
59
+ export function normalizeSlotKey(value: string | undefined): string {
60
+ return String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '');
61
+ }
62
+
63
+ export function buildCompoundSlotAliasMap(def: ComponentDef): Record<string, string> {
64
+ const out: Record<string, string> = {};
65
+ if (!def || def.type !== 'compound') return out;
66
+ const subComponents = Array.isArray(def.subComponents) ? def.subComponents : [];
67
+ for (let i = 0; i < subComponents.length; i++) {
68
+ const sub = subComponents[i];
69
+ const slotName = String(sub && sub.slot ? sub.slot : '').toLowerCase();
70
+ if (!slotName) continue;
71
+ const slotNorm = normalizeSlotKey(slotName);
72
+ if (slotNorm) out[slotNorm] = slotName;
73
+
74
+ const subNameNorm = normalizeSlotKey(String(sub && sub.name ? sub.name : ''));
75
+ if (subNameNorm && !out[subNameNorm]) out[subNameNorm] = slotName;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ export function extractCompoundSlotTextOverridesFromProps(def: ComponentDef, props: Record<string, unknown> | undefined): Record<string, string> {
81
+ const bySlot: Record<string, string> = {};
82
+ if (!def || def.type !== 'compound') return bySlot;
83
+ const slotAliases = buildCompoundSlotAliasMap(def);
84
+ const safeProps = props || {};
85
+ const keys = Object.keys(safeProps);
86
+ for (let i = 0; i < keys.length; i++) {
87
+ const key = keys[i];
88
+ const canonicalSlot = slotAliases[normalizeSlotKey(key)];
89
+ if (!canonicalSlot) continue;
90
+ const value = safeProps[key];
91
+ if (value == null) continue;
92
+ const text = String(value).trim();
93
+ if (!text) continue;
94
+ bySlot[canonicalSlot] = text;
95
+ }
96
+ return bySlot;
97
+ }
98
+
99
+ export function isSafeTextOverridePropForNonCompound(def: ComponentDef, key: string, value: unknown): boolean {
100
+ if (!def || def.type === 'compound' || def.type === 'cva') return false;
101
+ if (typeof value !== 'string' || value.trim() === '') return false;
102
+ const text = String(value).trim();
103
+ if (isUrlLikeText(text)) return false;
104
+ const precomputed = getPrecomputedSafeTextOverridePropKeys(def);
105
+ const inferred = inferSafeTextOverridePropKeys(def);
106
+ if (inferred.indexOf(key) !== -1) return true;
107
+ if (precomputed.length > 0) return false;
108
+ // Fallback for components with sparse story coverage: allow clearly human text values.
109
+ return isLikelyVisibleTextValue(text);
110
+ }
111
+
112
+ export function isSafeCompoundSlotOverrideProp(def: ComponentDef, key: string, value: unknown): boolean {
113
+ if (!def || def.type !== 'compound') return false;
114
+ if (value == null) return false;
115
+ const text = String(value).trim();
116
+ if (!text) return false;
117
+ return getCompoundSlotForPropKey(def, key) !== null;
118
+ }
119
+
120
+ export function getCompoundSlotForPropKey(def: ComponentDef, key: string): string | null {
121
+ const slotAliases = buildCompoundSlotAliasMap(def);
122
+ return slotAliases[normalizeSlotKey(key)] || null;
123
+ }
124
+
125
+ export function analyzeSymbolPolicy(def: ComponentDef, instance: ComponentInstance): SymbolPolicyAnalysis {
126
+ const props = instance && instance.props ? instance.props : {};
127
+ const ignoredProps: string[] = [];
128
+ const slotPropMappings: Record<string, string> = {};
129
+ if (splitClassName(props.className).length > 0) {
130
+ return { reason: 'class_override', ignoredProps, slotPropMappings };
131
+ }
132
+
133
+ // Keep boolean/semantic state props on fallback path for visual correctness.
134
+ if (props['aria-invalid'] != null && String(props['aria-invalid']).toLowerCase() !== 'false') {
135
+ return { reason: 'aria_invalid_prop', ignoredProps, slotPropMappings };
136
+ }
137
+ if (props.disabled != null && String(props.disabled).toLowerCase() !== 'false') {
138
+ return { reason: 'disabled_prop', ignoredProps, slotPropMappings };
139
+ }
140
+
141
+ const keys = Object.keys(props || {});
142
+ for (let i = 0; i < keys.length; i++) {
143
+ const key = keys[i];
144
+ if (IGNORED_SYMBOL_PROP_KEYS.has(key)) continue;
145
+ const value = props[key];
146
+ // Runtime/non-visual props should not force fallback rendering.
147
+ if (isEventHandlerPropKey(key)) {
148
+ pushUnique(ignoredProps, key);
149
+ continue;
150
+ }
151
+ if (isNonVisualA11yPropKey(key)) {
152
+ pushUnique(ignoredProps, key);
153
+ continue;
154
+ }
155
+ if (isNonVisualMetaPropKey(key)) {
156
+ pushUnique(ignoredProps, key);
157
+ continue;
158
+ }
159
+ if (isSafeTextOverridePropForNonCompound(def, key, value)) {
160
+ pushUnique(ignoredProps, key);
161
+ continue;
162
+ }
163
+ const mappedSlot = isSafeCompoundSlotOverrideProp(def, key, value)
164
+ ? getCompoundSlotForPropKey(def, key)
165
+ : null;
166
+ if (mappedSlot) {
167
+ pushUnique(ignoredProps, key);
168
+ slotPropMappings[key] = mappedSlot;
169
+ continue;
170
+ }
171
+ if (value == null) continue;
172
+ if (typeof value === 'string' && value.trim() === '') continue;
173
+ if (typeof value === 'function') continue;
174
+ return { reason: 'unsupported_prop:' + key, ignoredProps, slotPropMappings };
175
+ }
176
+ return { reason: null, ignoredProps, slotPropMappings };
177
+ }
178
+
179
+ export function getNonDefaultSymbolPropReason(def: ComponentDef, instance: ComponentInstance): string | null {
180
+ return analyzeSymbolPolicy(def, instance).reason;
181
+ }
182
+
183
+ function buildDefCacheKey(def: ComponentDef): string {
184
+ const name = String(def && def.name ? def.name : '');
185
+ const filePath = String(def && def.filePath ? def.filePath : '');
186
+ const type = String(def && def.type ? def.type : '');
187
+ return name + '|' + filePath + '|' + type;
188
+ }
189
+
190
+ function normalizeComponentName(name: string | undefined): string {
191
+ return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
192
+ }
193
+
194
+ function isMatchingInstanceForDef(def: ComponentDef, instance: ComponentInstance): boolean {
195
+ if (!def || !instance || !instance.componentName) return false;
196
+ return normalizeComponentName(instance.componentName) === normalizeComponentName(def.name);
197
+ }
198
+
199
+ function isLikelyVisibleTextValue(value: string): boolean {
200
+ const text = String(value || '').trim();
201
+ if (!text) return false;
202
+ if (isUrlLikeText(text)) return false;
203
+ if (/\s/.test(text)) return true;
204
+ if (/[.!?,:;()]/.test(text)) return true;
205
+ if (text.length >= 12) return true;
206
+ if (/^[A-Z]/.test(text)) return true;
207
+ return false;
208
+ }
209
+
210
+ function isUrlLikeText(value: string): boolean {
211
+ const text = String(value || '').trim().toLowerCase();
212
+ if (!text) return false;
213
+ return text.indexOf('http://') === 0
214
+ || text.indexOf('https://') === 0
215
+ || text.indexOf('mailto:') === 0
216
+ || text.indexOf('tel:') === 0
217
+ || text.indexOf('/') === 0
218
+ || text.indexOf('#') === 0;
219
+ }
220
+
221
+ function isTokenLikeText(value: string): boolean {
222
+ const text = String(value || '').trim();
223
+ if (!text) return false;
224
+ return /^[a-z0-9_-]+$/.test(text);
225
+ }
226
+
227
+ function buildTextPropInference(def: ComponentDef): Record<string, TextPropInferenceStats> {
228
+ const statsByKey: Record<string, TextPropInferenceStats> = {};
229
+ const stories = def && Array.isArray(def.stories) ? def.stories : [];
230
+ for (let i = 0; i < stories.length; i++) {
231
+ const story = stories[i];
232
+ const instances = story && Array.isArray(story.instances) ? story.instances : [];
233
+ for (let j = 0; j < instances.length; j++) {
234
+ const instance = instances[j];
235
+ if (!isMatchingInstanceForDef(def, instance)) continue;
236
+ const props = instance && instance.props ? instance.props : {};
237
+ const keys = Object.keys(props);
238
+ for (let k = 0; k < keys.length; k++) {
239
+ const key = keys[k];
240
+ if (IGNORED_SYMBOL_PROP_KEYS.has(key)) continue;
241
+ const value = props[key];
242
+ if (value == null) continue;
243
+ if (!statsByKey[key]) {
244
+ statsByKey[key] = {
245
+ stringCount: 0,
246
+ nonStringCount: 0,
247
+ uniqueValues: {},
248
+ hasLikelyTextValue: false,
249
+ hasUrlLikeValue: false,
250
+ hasTokenLikeValue: false,
251
+ };
252
+ }
253
+ const stats = statsByKey[key];
254
+ if (typeof value !== 'string') {
255
+ stats.nonStringCount++;
256
+ continue;
257
+ }
258
+ const text = value.trim();
259
+ if (!text) continue;
260
+ stats.stringCount++;
261
+ stats.uniqueValues[text] = true;
262
+ if (isLikelyVisibleTextValue(text)) stats.hasLikelyTextValue = true;
263
+ if (isUrlLikeText(text)) stats.hasUrlLikeValue = true;
264
+ if (isTokenLikeText(text)) stats.hasTokenLikeValue = true;
265
+ }
266
+ }
267
+ }
268
+ return statsByKey;
269
+ }
270
+
271
+ function getPrecomputedSafeTextOverridePropKeys(def: ComponentDef): string[] {
272
+ const raw = def && Array.isArray(def.safeTextOverrideProps)
273
+ ? def.safeTextOverrideProps
274
+ : [];
275
+ const out: string[] = [];
276
+ for (let i = 0; i < raw.length; i++) {
277
+ const key = String(raw[i] || '').trim();
278
+ if (!key) continue;
279
+ if (out.indexOf(key) !== -1) continue;
280
+ out.push(key);
281
+ }
282
+ return out;
283
+ }
284
+
285
+ export function inferSafeTextOverridePropKeys(def: ComponentDef): string[] {
286
+ if (!def || def.type === 'compound' || def.type === 'cva') return [];
287
+ const precomputed = getPrecomputedSafeTextOverridePropKeys(def);
288
+ if (precomputed.length > 0) return precomputed;
289
+ const cacheKey = buildDefCacheKey(def);
290
+ const cached = SAFE_TEXT_PROP_CACHE[cacheKey];
291
+ if (cached) return cached.slice();
292
+
293
+ const statsByKey = buildTextPropInference(def);
294
+ const out: string[] = [];
295
+ const keys = Object.keys(statsByKey);
296
+ for (let i = 0; i < keys.length; i++) {
297
+ const key = keys[i];
298
+ const stats = statsByKey[key];
299
+ if (!stats) continue;
300
+ if (isEventHandlerPropKey(key) || isNonVisualA11yPropKey(key) || isNonVisualMetaPropKey(key)) continue;
301
+ if (stats.nonStringCount > 0 || stats.stringCount === 0) continue;
302
+ const uniqueCount = Object.keys(stats.uniqueValues).length;
303
+ const hasEvidence = stats.hasLikelyTextValue || uniqueCount > 1;
304
+ if (!hasEvidence) continue;
305
+ // Avoid props that look purely URL/token-like across all observed stories.
306
+ if (stats.hasUrlLikeValue && !stats.hasLikelyTextValue) continue;
307
+ if (stats.hasTokenLikeValue && !stats.hasLikelyTextValue && uniqueCount <= 1) continue;
308
+ out.push(key);
309
+ }
310
+ SAFE_TEXT_PROP_CACHE[cacheKey] = out.slice();
311
+ return out;
312
+ }
@@ -0,0 +1,130 @@
1
+ import type { ComponentDef } from '../components';
2
+
3
+ /**
4
+ * Finds existing component-block frames on a page (or any subtree). Two
5
+ * matching strategies, tried in order:
6
+ *
7
+ * 1. `inkbridge:role` plugin data set to `component-block:...:<name>` — the
8
+ * canonical path. Survives user-driven frame renames and is set by the
9
+ * builder when blocks are generated.
10
+ * 2. Legacy name + structural fallback — frame's own name matches AND it
11
+ * contains a `Story Layout` child. Catches blocks generated before the
12
+ * plugin data tagging landed.
13
+ */
14
+ export function findComponentBlocksInPage(root: BaseNode, name: string): SceneNode[] {
15
+ const out: SceneNode[] = [];
16
+ if (!root || !('children' in root) || !Array.isArray(root.children)) return out;
17
+
18
+ function getGeneratedRole(node: SceneNode): string {
19
+ try {
20
+ return (node && typeof node.getPluginData === 'function')
21
+ ? (node.getPluginData('inkbridge:role') || '')
22
+ : '';
23
+ } catch (_e) {
24
+ return '';
25
+ }
26
+ }
27
+
28
+ function roleMatchesComponent(role: string, componentName: string): boolean {
29
+ if (!role || role.indexOf('component-block:') !== 0) return false;
30
+ const parts = role.split(':');
31
+ if (parts.length < 4) return false;
32
+ return parts[parts.length - 1] === componentName;
33
+ }
34
+
35
+ const visit = (node: BaseNode): void => {
36
+ if (!node || !('children' in node) || !Array.isArray(node.children)) return;
37
+ for (let i = 0; i < node.children.length; i++) {
38
+ const child = node.children[i];
39
+ if (!child) continue;
40
+ const role = getGeneratedRole(child);
41
+ // Primary matching path: generated metadata is explicit and resilient to
42
+ // user renames or future block node type changes.
43
+ if (roleMatchesComponent(role, name)) {
44
+ out.push(child);
45
+ continue;
46
+ }
47
+ // Legacy fallback: match by structure for older untagged blocks.
48
+ if (!role && child.name === name && 'children' in child && Array.isArray(child.children)) {
49
+ let hasStoryLayout = false;
50
+ for (let ci = 0; ci < child.children.length; ci++) {
51
+ if (child.children[ci] && child.children[ci].name === 'Story Layout') {
52
+ hasStoryLayout = true;
53
+ break;
54
+ }
55
+ }
56
+ if (hasStoryLayout) {
57
+ out.push(child);
58
+ continue;
59
+ }
60
+ }
61
+ if ('children' in child) visit(child);
62
+ }
63
+ };
64
+
65
+ visit(root);
66
+ return out;
67
+ }
68
+
69
+ /**
70
+ * Parses a stored `inkbridge:hash` plugin-data value into its parts. Stored
71
+ * format is `defHash:tokenHash:engineHash` (engineHash optional for legacy
72
+ * pre-engine-aware blocks). Returns null for unparseable input.
73
+ */
74
+ export function parseStoredBlockHash(value: string | null): { defHash: string; tokenHash: string; engineHash: string | null } | null {
75
+ if (!value || typeof value !== 'string') return null;
76
+ const parts = value.split(':');
77
+ if (parts.length < 2) return null;
78
+ return {
79
+ defHash: parts[0],
80
+ tokenHash: parts[1],
81
+ engineHash: parts.length > 2 ? parts.slice(2).join(':') : null,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * True when the stored hash represents the same def + token as the current
87
+ * render. The engine segment is tolerated as a free variable so render-engine
88
+ * upgrades don't force every block to rebuild; the def+token equality is the
89
+ * load-bearing check.
90
+ */
91
+ export function blockHashMatchesCurrent(
92
+ storedHash: string | null,
93
+ defHash: string,
94
+ tokenHash: string,
95
+ engineHash: string
96
+ ): boolean {
97
+ if (!storedHash) return false;
98
+ const exact = defHash + ':' + tokenHash + ':' + engineHash;
99
+ if (storedHash === exact) return true;
100
+
101
+ // Backward compatibility:
102
+ // - pre-engine format: def:token
103
+ // - engine/hash migration: same def+token but different engine segment
104
+ const parsed = parseStoredBlockHash(storedHash);
105
+ return !!parsed && parsed.defHash === defHash && parsed.tokenHash === tokenHash;
106
+ }
107
+
108
+ /**
109
+ * Mirror of `normalizeComponentDef` in ui-builder.ts (no spread — `Object.assign`).
110
+ * Keeps computePreflightData independent of ui-builder to avoid circular
111
+ * imports. Hoists `def.analysis` fields onto the top-level def shape while
112
+ * preserving classification metadata (`kind`, `usesCount`, etc.) from the raw
113
+ * scanner output.
114
+ */
115
+ export function normalizeForPreflight(raw: ComponentDef): ComponentDef {
116
+ if (!raw || !raw.analysis) return raw;
117
+ return Object.assign({}, raw.analysis, {
118
+ filePath: raw.analysis.filePath || raw.filePath,
119
+ stories: Array.isArray(raw.analysis.stories)
120
+ ? raw.analysis.stories
121
+ : (Array.isArray(raw.stories) ? raw.stories : []),
122
+ hasStory: typeof raw.analysis.hasStory === 'boolean'
123
+ ? raw.analysis.hasStory
124
+ : !!raw.hasStory,
125
+ kind: raw.kind,
126
+ usesCount: raw.usesCount,
127
+ usedByCount: raw.usedByCount,
128
+ isLeaf: raw.isLeaf,
129
+ });
130
+ }