inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
|
@@ -0,0 +1,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,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
|
+
}
|