@tinkerlandai/indiana-iam 0.3.0 → 0.4.0

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/src/analyze.mjs CHANGED
@@ -1,4 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
+ import {referencedRasterSources} from "./source-media.mjs";
3
+ import {uiEnvironmentPrimitive} from "./source-environment.mjs";
4
+ import {sourceChart} from "./source-charts.mjs";
2
5
  import path from "node:path";
3
6
  import { automaticSourceAnchors, anchorAttributes, sourceScriptKind } from "./anchors.mjs";
4
7
  import { sourceFiles } from "./source-files.mjs";
@@ -7,9 +10,16 @@ import { assembleRouteTree, readVariables } from "./route-tree.mjs";
7
10
  import { sourceEntry } from "./source-entry.mjs";
8
11
  import { frameworkRoutes, externalImport } from "./framework-routes.mjs";
9
12
  import { sourceResolution } from "./source-resolution.mjs";
10
- import { routeIndexDigest, refreshRenderHashes } from "./render-index.mjs";
13
+ import { routeIndexDigest, refreshRenderHashes, flattenComposition } from "./render-index.mjs";
11
14
  import { storeDocumentNodes } from "./node-hashes.mjs";
12
15
  import { staticQuery } from "./static-query.mjs";
16
+ import { attachUiStructure } from './ui-structure.mjs';
17
+ import { componentRegistry } from './component-registry.mjs';
18
+ import { sourceRedirects } from './source-redirects.mjs';
19
+ import {internGraphSources} from './graph-sources.mjs';
20
+ import {collectionStateReads,sourceCollectionFragment} from './source-collections.mjs';
21
+ import {sourceQueryEvidence} from './source-query-evidence.mjs';
22
+ import {lucideSource,lucideGeometry,LUCIDE_SOURCE,LUCIDE_SOURCE_VERSION} from './lucide-source.mjs';
13
23
  import {
14
24
  publicRoute,
15
25
  assertRoutes,
@@ -29,6 +39,27 @@ const { JSDOM } = dependency("jsdom").value;
29
39
  const UNKNOWN = Symbol("unknown");
30
40
  import { sensitiveName, redactLiteral, privateUrl } from "./privacy.mjs";
31
41
  const isUnknown = (value) => value === UNKNOWN;
42
+ const COMPONENT=Symbol('source-component');
43
+ const TRANSLATION=Symbol('source-translation-catalog');
44
+ const UI_SETTER=Symbol('source-ui-setter');
45
+ const UI_PRIMITIVE=Symbol('source-ui-primitive');
46
+
47
+ function uiReturnExpression(fn) {
48
+ if(!fn?.body||!ts.isBlock(fn.body))return returnExpression(fn);
49
+ const returned=statement=>ts.isReturnStatement(statement)?statement.expression:ts.isBlock(statement)&&statement.statements.every(s=>ts.isVariableStatement(s)||ts.isReturnStatement(s))?returned(statement.statements.at(-1)):null;
50
+ function sequence(statements) {
51
+ for(let i=0;i<statements.length;i++) {
52
+ const statement=statements[i];
53
+ if(ts.isReturnStatement(statement))return statement.expression;
54
+ if(ts.isIfStatement(statement)) {
55
+ const yes=returned(statement.thenStatement),no=statement.elseStatement?returned(statement.elseStatement):sequence(statements.slice(i+1));
56
+ if(yes&&no)return {kind:ts.SyntaxKind.ConditionalExpression,condition:statement.expression,whenTrue:yes,whenFalse:no};
57
+ }
58
+ }
59
+ return returnExpression(fn);
60
+ }
61
+ return sequence([...fn.body.statements]);
62
+ }
32
63
  // React numeric styles use px except for unitless properties and CSS variables.
33
64
  const unitlessStyles = new Set("animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth".split(" "));
34
65
  function staticStyle(value, unsupported) {
@@ -171,6 +202,8 @@ export async function analyze({
171
202
  components = new Map(),
172
203
  assets = new Map();
173
204
  const cssSources = [];
205
+ const jsonResources=new Map(),translationResources=new Set();
206
+ const presentation={conditions:[],tabs:[],navigation:[],regions:[],collections:[],fragments:[],attributes:[],attributeSets:[]},collectionTemplates=new Map();
174
207
  const ui = { variables: [], bindings: [], transitions: [], uncovered: [] };
175
208
  const uiVariableKeys = new Set();
176
209
  let byteCount = 0;
@@ -200,6 +233,7 @@ export async function analyze({
200
233
  sha256: sha256(bytes),
201
234
  bytes: bytes.length,
202
235
  });
236
+ if(file.endsWith('.json')){try{jsonResources.set(file,JSON.parse(text));}catch{/* Non-resource JSON stays opaque. */}continue;}
203
237
  if (file.endsWith(".css")) {
204
238
  gap(
205
239
  file.endsWith(".module.css")
@@ -211,7 +245,11 @@ export async function analyze({
211
245
  if (!file.endsWith(".module.css")) cssSources.push({ file, text });
212
246
  continue;
213
247
  }
214
- if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
248
+ if (!/\.[cm]?[jt]sx?$/.test(file)) {
249
+ if (/^(?:src\/)?app\/(?:.*\/)?(?:page|layout)\.mdx$/.test(file) || /^(?:src\/)?pages\/.+\.mdx$/.test(file))
250
+ gap("ROUTE_MDX_UNSUPPORTED", file, "MDX route/layout source requires a dedicated adapter; its page and layout coverage is incomplete");
251
+ continue;
252
+ }
215
253
  const source = ts.createSourceFile(
216
254
  file,
217
255
  text,
@@ -338,6 +376,52 @@ export async function analyze({
338
376
  .find(Boolean) || null
339
377
  );
340
378
  }
379
+ function jsonResource(from,specifier){
380
+ let file=specifier.startsWith('.')?path.posix.normalize(path.posix.join(path.posix.dirname(from),specifier)):null;
381
+ if(!file)for(const [pattern,values] of Object.entries(alias)){const prefix=pattern.split('*')[0];if(pattern.includes('*')&&specifier.startsWith(prefix)){file=values[0].replace('*',specifier.slice(prefix.length)).replace(/^\.\//,'');break;}}
382
+ return file&&!file.startsWith('../')?jsonResources.get(file):undefined;
383
+ }
384
+ function contextKey(module,name){
385
+ const ref=binding(module,name),call=ref&&unwrap(ref.node);
386
+ const primitive=call&&ts.isCallExpression(call)&&externalImport(ref.module,call.expression.getText(ref.module.source));
387
+ return primitive?.specifier==='react'&&primitive.name==='createContext'?ref.module.file+'#'+ref.name:null;
388
+ }
389
+ function sourceContextCall(node,module,ctx){
390
+ const imported=externalImport(module,node.expression.getText(module.source));
391
+ if(imported?.specifier==='react'&&imported.name==='useContext'&&ts.isIdentifier(node.arguments[0]))return ctx.__iamContexts?.get(contextKey(module,node.arguments[0].text))??UNKNOWN;
392
+ const ref=ts.isIdentifier(node.expression)&&binding(module,node.expression.text),fn=ref&&functionFrom(ref.node);
393
+ if(!fn?.body||!ts.isBlock(fn.body))return UNKNOWN;
394
+ const result=unwrap(returnExpression(fn));
395
+ if(!result||!ts.isIdentifier(result))return UNKNOWN;
396
+ for(const statement of fn.body.statements)if(ts.isVariableStatement(statement))for(const decl of statement.declarationList.declarations){
397
+ const call=unwrap(decl.initializer),primitive=call&&ts.isCallExpression(call)&&externalImport(ref.module,call.expression.getText(ref.module.source));
398
+ if(primitive?.specifier==='react'&&primitive.name==='useContext'&&ts.isIdentifier(decl.name)&&decl.name.text===result.text&&ts.isIdentifier(call.arguments[0]))return ctx.__iamContexts?.get(contextKey(ref.module,call.arguments[0].text))??UNKNOWN;
399
+ if(primitive?.specifier==='react-i18next'&&primitive.name==='useTranslation'&&ts.isObjectBindingPattern(decl.name)&&decl.name.elements.some(e=>(e.propertyName?.text||e.name.text)==='t'&&e.name.text===result.text)){
400
+ const catalogs=[...ref.module.imports.values()].filter(i=>i.specifier.endsWith('.json')).map(i=>jsonResource(ref.module.file,i.specifier)).filter(Boolean);
401
+ if(catalogs.length===1){translationResources.add(catalogs[0]);return {[TRANSLATION]:catalogs[0]};}
402
+ }
403
+ }
404
+ return UNKNOWN;
405
+ }
406
+ function basePrimitive(module,tag,seen=new Set()){
407
+ if(seen.has(module.file+'#'+tag)||seen.size>8)return null;seen.add(module.file+'#'+tag);
408
+ const [name,part,extra]=tag.split('.'),ref=module.imports.get(name);
409
+ const imported=externalImport(module,tag);
410
+ const radixFamilies={'dialog':'Dialog','alert-dialog':'AlertDialog','popover':'Popover','dropdown-menu':'Menu','context-menu':'Menu','tooltip':'Tooltip','hover-card':'Popover','tabs':'Tabs','collapsible':'Collapsible'};
411
+ const radix=imported?.specifier?.match(/^@radix-ui\/react-([a-z-]+)$/)?.[1];
412
+ if(radixFamilies[radix]) {
413
+ const family=radixFamilies[radix],prefix=radix.split('-').map(v=>v[0].toUpperCase()+v.slice(1)).join('');
414
+ const exported=imported.name===prefix?'Root':imported.name.startsWith(prefix)?imported.name.slice(prefix.length):imported.name;
415
+ const mapped=family==='Tabs'?({Trigger:'Tab',Content:'Panel'}[exported]??exported):({Content:'Popup',Overlay:'Backdrop',Sub:'SubmenuRoot',SubTrigger:'SubmenuTrigger',SubContent:'Popup'}[exported]??exported);
416
+ return {family,part:mapped};
417
+ }
418
+ if(imported?.specifier==='react-transition-group'&&['Transition','CSSTransition'].includes(imported.name))return {family:'Transition',part:'Root'};
419
+ const families={'@base-ui/react/tabs':'Tabs','@base-ui/react/popover':'Popover','@base-ui/react/dialog':'Dialog','@base-ui/react/alert-dialog':'AlertDialog','@base-ui/react/menu':'Menu','@base-ui/react/tooltip':'Tooltip'};
420
+ if(!extra&&part&&ref&&families[ref.specifier]===ref.name)return {family:ref.name,part};
421
+ if(!part){const target=binding(module,tag),node=target&&unwrap(target.node);if(node&&ts.isPropertyAccessExpression(node))return basePrimitive(target.module,node.getText(target.module.source),seen);}
422
+ return null;
423
+ }
424
+ function tabsPrimitive(module,tag){const primitive=basePrimitive(module,tag);return primitive?.family==='Tabs'&&['Root','List','Tab','Panel','Indicator'].includes(primitive.part)?primitive.part:null;}
341
425
  function binding(module, name, seen=new Set()) {
342
426
  if(!name||seen.size>=16||seen.has(module.file+'#'+name))return null;seen.add(module.file+'#'+name);
343
427
  if (module.bindings.has(name))
@@ -352,7 +436,7 @@ export async function analyze({
352
436
  : target.exports.get(imported.name) || imported.name;
353
437
  return binding(target,key,seen);
354
438
  }
355
- const componentDefs = new Map();
439
+ const componentDefs = new Map(),componentsByFunction=new WeakMap();
356
440
  function registerComponent(module, symbol, node) {
357
441
  const fn = functionFrom(node);
358
442
  if (!fn) return null;
@@ -377,7 +461,7 @@ export async function analyze({
377
461
  ts.forEachChild(n, number);
378
462
  }
379
463
  number(fn);
380
- const def = { module, symbol, fn, key, ordinals };
464
+ const def = { module, symbol, fn, key, ordinals };componentsByFunction.set(fn,def);
381
465
  let returns = 0;
382
466
  function countReturns(item) {
383
467
  if (
@@ -459,6 +543,17 @@ export async function analyze({
459
543
  fileHash: module.hash,
460
544
  };
461
545
  }
546
+ function sourceComponent(node,module){
547
+ const imported=ts.isIdentifier(node)&&externalImport(module,node.text);if(imported?.specifier==='@radix-ui/react-slot'&&['Slot','Root'].includes(imported.name))return {[UI_PRIMITIVE]:imported};if(imported?.specifier==='lucide-react'){const icon=lucideSource(imported.name);if(icon)return icon;}
548
+ const ref=ts.isIdentifier(node)?binding(module,node.text):null;const def=componentsByFunction.get(node)??(ref&&componentDefs.get(ref.module.file+'#'+ref.name));
549
+ return def?{[COMPONENT]:def}:null;
550
+ }
551
+ function environment(node,module,context){
552
+ const value=uiEnvironmentPrimitive({ts,node,module,binding,imported:externalImport,evaluate:evalExpression});
553
+ if(value?.kind==='empty-preference'){gap('UI_PREFERENCE_EMPTY_PREVIEW',module.file,'Source-declared SSR fallback for a UI preference; preview starts without cookies, never reads merchant browser state.');return [undefined];}
554
+ return value;
555
+ }
556
+ function query(node,module,context){return staticQuery({ts,node,module,context,binding,unknown:UNKNOWN,redact:redactLiteral,sensitive:sensitiveName,reference:sourceComponent,isReference:v=>Boolean(v?.[COMPONENT]||v?.[LUCIDE_SOURCE]||v?.[UI_PRIMITIVE]),imported:externalImport,project:(n,m,c)=>{const v=environment(n,m,c);return Array.isArray(v)?v:null;}});}
462
557
  function evalExpression(
463
558
  node,
464
559
  module,
@@ -469,6 +564,7 @@ export async function analyze({
469
564
  node = unwrap(node);
470
565
  if (!node || depth > 30) return UNKNOWN;
471
566
  const ev = (n) => evalExpression(n, module, context, stack, depth + 1);
567
+ const component=!(ts.isIdentifier(node)&&Object.hasOwn(context,node.text))&&sourceComponent(node,module);if(component)return component;
472
568
  if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node))
473
569
  return ts.isNumericLiteral(node) ? Number(node.text) : redactLiteral(node.text);
474
570
  if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
@@ -476,7 +572,14 @@ export async function analyze({
476
572
  if (node.kind === ts.SyntaxKind.NullKeyword) return null;
477
573
  if (ts.isIdentifier(node)) {
478
574
  if (Object.hasOwn(context, node.text)) return context[node.text];
575
+ const variableKey=context.__iamVariableKeys?.[context.__iamSetters?.[node.text]];
576
+ if(variableKey)return {[UI_SETTER]:{variableKey,value:{op:'event',key:'value'},payload:{value:ui.variables.find(v=>v.variableKey===variableKey)?.valueType}}};
479
577
  if (node.text === "undefined") return undefined;
578
+ const image=module.imports.get(node.text);
579
+ if(image?.name==='default'&&/^\.\.?\/[^?#]+\.(?:png|jpe?g|webp|gif)$/i.test(image.specifier)){
580
+ const file=path.posix.normalize(path.posix.join(path.posix.dirname(module.file),image.specifier));
581
+ if(!file.startsWith('../')&&!file.split('/').some(p=>p.startsWith('.'))&&!sensitiveName(file))return '/__iam_source__/'+file;
582
+ }
480
583
  const token = module.file + "#" + node.text;
481
584
  if (stack.has(token)) return UNKNOWN;
482
585
  const ref = binding(module, node.text);
@@ -487,12 +590,16 @@ export async function analyze({
487
590
  if (ts.isArrayLiteralExpression(node)) return node.elements.map(ev);
488
591
  if (ts.isObjectLiteralExpression(node)) {
489
592
  const obj = {};
593
+ const fields=new Map();
490
594
  for (const prop of node.properties) {
491
595
  if (ts.isPropertyAssignment(prop))
492
596
  obj[prop.name.getText(module.source).replace(/^['"]|['"]$/g, "")] =
493
597
  ev(prop.initializer);
494
- else if (ts.isShorthandPropertyAssignment(prop))
598
+ else if (ts.isShorthandPropertyAssignment(prop)){
495
599
  obj[prop.name.text] = ev(prop.name);
600
+ const variable=context.__iamVariableKeys?.[prop.name.text],inherited=context.__iamPropBindings?.get(prop.name.text);
601
+ if(variable||inherited)fields.set(prop.name.text,inherited||{op:'var',key:variable});
602
+ }
496
603
  else if (ts.isSpreadAssignment(prop)) {
497
604
  const v = ev(prop.expression);
498
605
  if (isUnknown(v)) return UNKNOWN;
@@ -500,6 +607,7 @@ export async function analyze({
500
607
  Object.assign(obj, v);
501
608
  }
502
609
  }
610
+ if(fields.size)objectBindings.set(obj,fields);
503
611
  return obj;
504
612
  }
505
613
  if (
@@ -553,6 +661,12 @@ export async function analyze({
553
661
  return isUnknown(a) ? UNKNOWN : (a ?? b);
554
662
  if (isUnknown(a) || isUnknown(b)) return UNKNOWN;
555
663
  if (op === ts.SyntaxKind.PlusToken) return a + b;
664
+ if(op === ts.SyntaxKind.MinusToken && typeof a==='number' && typeof b==='number')return a-b;
665
+ if(['number','string'].includes(typeof a)&&typeof a===typeof b){
666
+ if(op===ts.SyntaxKind.GreaterThanToken)return a>b;if(op===ts.SyntaxKind.GreaterThanEqualsToken)return a>=b;
667
+ if(op===ts.SyntaxKind.LessThanToken)return a<b;if(op===ts.SyntaxKind.LessThanEqualsToken)return a<=b;
668
+ }
669
+ if(op===ts.SyntaxKind.InKeyword&&typeof a==='string'&&b&&typeof b==='object'&&!['__proto__','constructor','prototype'].includes(a))return Object.hasOwn(b,a);
556
670
  if (
557
671
  [
558
672
  ts.SyntaxKind.EqualsEqualsEqualsToken,
@@ -570,9 +684,21 @@ export async function analyze({
570
684
  }
571
685
  if (ts.isCallExpression(node)) {
572
686
  const callee = node.expression.getText(module.source);
687
+ const projected=environment(node,module,context);if(Array.isArray(projected))return projected[0];
688
+ const renderCallback=ev(node.expression)?.iamRenderProp;
689
+ if(renderCallback)return {iamRenderInvocation:{callback:renderCallback,args:node.arguments.map(ev),caller:context}};
690
+ const primitive=externalImport(module,callee);
691
+ if(['react-router','react-router-dom'].includes(primitive?.specifier)&&primitive.name==='useLocation'&&typeof context.__iamRoutePath==='string')return {pathname:context.__iamRoutePath,search:UNKNOWN,hash:UNKNOWN};
692
+ const translation=ts.isIdentifier(node.expression)&&context[node.expression.text]?.[TRANSLATION];
693
+ if(translation){const key=ev(node.arguments[0]);if(typeof key!=='string')return UNKNOWN;const value=key.split('.').reduce((value,key)=>value&&Object.hasOwn(Object(value),key)?value[key]:undefined,translation);return typeof value==='string'&&!/\{\{/.test(value)?redactLiteral(value):UNKNOWN;}
694
+ if(primitive?.specifier==='react'&&primitive.name==='useMemo'&&node.arguments.length===2){
695
+ const fn=node.arguments[0];if(ts.isArrowFunction(fn)&&!fn.modifiers?.length&&!fn.parameters.length&&!ts.isBlock(fn.body))return ev(fn.body);
696
+ }
697
+ const contextValue=sourceContextCall(node,module,context);
698
+ if(!isUnknown(contextValue))return contextValue;
573
699
  if (/(?:^|\.)useState$/.test(callee))
574
700
  return {
575
- stateInitial: node.arguments.length ? ev(node.arguments[0]) : null,
701
+ stateInitial: node.arguments.length ? (ts.isArrowFunction(node.arguments[0])&&node.arguments[0].parameters.length===0 ? query({kind:ts.SyntaxKind.CallExpression,expression:node.arguments[0],arguments:[]},module,context) : ev(node.arguments[0])) : null,
576
702
  };
577
703
  if (callee === "cn" || callee === "clsx") {
578
704
  let valid=true;
@@ -607,6 +733,7 @@ export async function analyze({
607
733
  ([key, values]) =>
608
734
  values[isUnknown((props || {})[key]) ? options.defaultVariants?.[key] : (props || {})[key] ?? options.defaultVariants?.[key]],
609
735
  ),
736
+ ...(options?.compoundVariants??[]).flatMap(row=>Object.entries(row).filter(([key])=>!['class','className'].includes(key)).every(([key,value])=>{const actual=Object.hasOwn(props??{},key)&&props[key]!==undefined?props[key]:options.defaultVariants?.[key];return Array.isArray(value)?value.includes(actual):value===actual;})?[row.class,row.className]:[]),
610
737
  props?.class,
611
738
  props?.className,
612
739
  ]
@@ -617,7 +744,7 @@ export async function analyze({
617
744
  const target = ev(node.expression.expression),
618
745
  args = node.arguments.map(ev),
619
746
  method = node.expression.name.text;
620
- if (isUnknown(target) || args.some(isUnknown)) return staticQuery({ ts, node, module, context, binding, unknown: UNKNOWN, redact: redactLiteral, sensitive: sensitiveName });
747
+ if (isUnknown(target) || args.some(isUnknown)) return query(node,module,context);
621
748
  if (
622
749
  typeof target === "string" &&
623
750
  [
@@ -633,7 +760,7 @@ export async function analyze({
633
760
  return target.filter((v) => !isUnknown(v)).join(args[0]);
634
761
  }
635
762
  }
636
- if (ts.isCallExpression(node)) return staticQuery({ ts, node, module, context, binding, unknown: UNKNOWN, redact: redactLiteral, sensitive: sensitiveName });
763
+ if (ts.isCallExpression(node)) return query(node,module,context);
637
764
  return UNKNOWN;
638
765
  }
639
766
  // Preserve only definitely present class tokens when a template contains an
@@ -689,15 +816,14 @@ export async function analyze({
689
816
  const omitted = new Set(param.name.elements.filter(item => !item.dotDotDotToken).map(item => item.propertyName?.getText() || item.name.getText()));
690
817
  const rest = Object.fromEntries(Object.entries(props).filter(([key]) => !omitted.has(key)));
691
818
  const parent = forwarded.get(props);
692
- if (parent) forwarded.set(rest, { bindings: parent.bindings.filter(b => !omitted.has(b.property)), handlers: parent.handlers.filter(h => !omitted.has(h.key)) });
819
+ if (parent) forwarded.set(rest, { bindings: parent.bindings.filter(b => !omitted.has(b.property)), handlers: parent.handlers.filter(h => !omitted.has(h.key)), attributes:(parent.attributes??[]).filter(a=>!omitted.has(a.key)) });
693
820
  ctx[el.name.text] = rest; continue;
694
821
  }
695
822
  const key = el.propertyName?.getText() || el.name.getText();
696
823
  ctx[el.name.getText()] =
697
- props[key] ??
698
- (el.initializer
699
- ? evalExpression(el.initializer, def.module, ctx)
700
- : UNKNOWN);
824
+ Object.hasOwn(props, key) && props[key] !== undefined ? props[key] :
825
+ (el.initializer ? evalExpression(el.initializer, def.module, ctx) :
826
+ props.__iamOpaqueProps ? UNKNOWN : undefined);
701
827
  const inherited = forwarded.get(props);
702
828
  const binding = inherited?.bindings.find(b => b.property === key), handler = inherited?.handlers.find(h => h.key === key);
703
829
  if (binding) ctx.__iamPropBindings.set(el.name.getText(), binding.value);
@@ -709,8 +835,22 @@ export async function analyze({
709
835
  if (ts.isVariableStatement(s))
710
836
  for (const d of s.declarationList.declarations) {
711
837
  if (ts.isIdentifier(d.name)) {
712
- ctx[d.name.text] = sensitiveName(d.name.text) ? UNKNOWN : evalExpression(d.initializer, def.module, ctx);
838
+ const projected=environment(d.initializer,def.module,ctx);
839
+ if(projected?.kind==='viewport'){
840
+ const width=Number(projected.query.match(/\d+/)[0]);ctx[d.name.text]=projected.query.includes('max-width')?1280<=width:1280>=width;ctx.__iamStates.add(d.name.text);ctx.__iamStateNodes[d.name.text]=d;ctx.__iamViewportQueries??={};ctx.__iamViewportQueries[d.name.text]=projected.query;
841
+ gap('VIEWPORT_SOURCE_PROJECTION',def.module.file,'Source matchMedia query projected against the preview viewport; subscription effects are not executed.');
842
+ }else ctx[d.name.text] = sensitiveName(d.name.text) ? UNKNOWN : evalExpression(d.initializer, def.module, ctx);
713
843
  ctx.__iamLocals.set(d.name.text, d.initializer);
844
+ } else if(ts.isObjectBindingPattern(d.name)){
845
+ const value=evalExpression(d.initializer,def.module,ctx);
846
+ for(const field of d.name.elements)if(ts.isIdentifier(field.name)&&!field.dotDotDotToken){
847
+ const key=field.propertyName?.text||field.name.text;
848
+ ctx[field.name.text]=!isUnknown(value)&&value&&typeof value==='object'&&Object.hasOwn(value,key)?value[key]:field.initializer?evalExpression(field.initializer,def.module,ctx):UNKNOWN;
849
+ const inherited=value&&typeof value==='object'&&objectBindings.get(value)?.get(key);
850
+ if(inherited)ctx.__iamPropBindings.set(field.name.text,inherited);
851
+ const setter=value?.[key]?.[UI_SETTER];
852
+ if(setter)ctx.__iamPropHandlers.set(field.name.text,setter);
853
+ }
714
854
  } else if (ts.isArrayBindingPattern(d.name)) {
715
855
  const call = unwrap(d.initializer), hook = call && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && def.module.imports.get(call.expression.text);
716
856
  if (hook?.specifier === "react" && hook.name === "useActionState") {
@@ -740,7 +880,7 @@ export async function analyze({
740
880
  continue;
741
881
  }
742
882
  const key = stateBinding.name.text;
743
- ctx[key] = sensitiveName(key) ? UNKNOWN : value.stateInitial;
883
+ ctx[key] = sensitiveName(key) && typeof value.stateInitial !== 'boolean' ? UNKNOWN : value.stateInitial;
744
884
  ctx.__iamStates.add(key);
745
885
  ctx.__iamStateNodes[key] = d;
746
886
  if (d.name.elements[1]?.name && !d.name.elements[1].dotDotDotToken && ts.isIdentifier(d.name.elements[1].name))
@@ -858,19 +998,20 @@ export async function analyze({
858
998
  };
859
999
  const record = new Map(),
860
1000
  used = new Set(),
861
- branches = [];
1001
+ branches = [],attributeRequests=[];
862
1002
  let renderedCount = 0;
863
1003
  function renderComponent(def, props, instance, depth) {
864
1004
  if (depth === 0 && composition.entryMode === "soft") instance = composition.compositionKey + ":" + instance;
865
1005
  // Component recursion and JSX nesting are separate budgets. Counting DOM
866
1006
  // wrappers as component calls silently dropped ordinary nested navigation.
867
- if (used.size >= 18 || used.has(def.key + ":" + instance)) {
1007
+ if (used.size >= 24 || used.has(def.key + ":" + instance)) {
868
1008
  gap("COMPONENT_DEPTH_LIMIT", def.module.file, def.symbol + ": active component call budget exceeded");
869
1009
  return "";
870
1010
  }
871
1011
  used.add(def.key + ":" + instance);
872
1012
  if (!page.componentKeys.includes(def.key))
873
1013
  page.componentKeys.push(def.key);
1014
+ props.__iamRoutePath=/[\[\]]/.test(route)?UNKNOWN:route;
874
1015
  const ctx = componentContext(def, props);
875
1016
  ctx.__iamOutlet = props.__iamOutlet;
876
1017
  ctx.__iamScope = identity("state-scope", {
@@ -895,11 +1036,17 @@ export async function analyze({
895
1036
  }
896
1037
  if (!uiVariableKeys.has(variableKey)) {
897
1038
  uiVariableKeys.add(variableKey);
898
- ui.variables.push({ variableKey, routeKey: pageKey, scopeKey: ctx.__iamScope, label, valueType: typeof initialValue, initialValue, source: sourceLocation(def.module, ctx.__iamStateNodes[label], def.symbol) });
1039
+ ui.variables.push({ variableKey, routeKey: pageKey, scopeKey: ctx.__iamScope, label:ctx.__iamViewportQueries?.[label]?'@media '+ctx.__iamViewportQueries[label]:label, valueType: typeof initialValue, initialValue, source: sourceLocation(def.module, ctx.__iamStateNodes[label], def.symbol) });
899
1040
  }
900
1041
  }
1042
+ // Rebind pure local records after scalar state identities exist. This preserves
1043
+ // useMemo/context field relationships without invoking a hook or callback.
1044
+ for(const [name,expression]of ctx.__iamLocals){
1045
+ if(ctx.__iamStates.has(name)||sensitiveName(name))continue;
1046
+ const value=evalExpression(expression,def.module,ctx);if(!isUnknown(value))ctx[name]=value;
1047
+ }
901
1048
  const result = renderExpr(
902
- returnExpression(def.fn),
1049
+ uiReturnExpression(def.fn),
903
1050
  def,
904
1051
  ctx,
905
1052
  instance,
@@ -934,6 +1081,7 @@ export async function analyze({
934
1081
  }
935
1082
  if (ts.isIdentifier(node)) {
936
1083
  if (Object.hasOwn(aliases, node.text)) return aliases[node.text];
1084
+ if(eventParam&&node.text===eventParam)return {op:'event',key:'value'};
937
1085
  if (ctx.__iamVariableKeys[node.text]) return { op: "var", key: ctx.__iamVariableKeys[node.text] };
938
1086
  if (ctx.__iamPropBindings.has(node.text)) return ctx.__iamPropBindings.get(node.text);
939
1087
  if (ctx.__iamLocals.has(node.text)) return compile(ctx.__iamLocals.get(node.text));
@@ -976,44 +1124,371 @@ export async function analyze({
976
1124
  if (["add", "subtract"].includes(expression.op)) return left === "number" && right === "number" ? "number" : undefined;
977
1125
  return undefined;
978
1126
  }
979
- function uiHandler(initializer, owner, ctx) {
1127
+ function uiHandler(initializer, owner, ctx, property = '') {
980
1128
  let fn = initializer && ts.isJsxExpression(initializer) ? unwrap(initializer.expression) : null;
981
- if (fn && ts.isIdentifier(fn) && ctx.__iamPropHandlers.has(fn.text)) return ctx.__iamPropHandlers.get(fn.text);
982
- if (fn && ts.isIdentifier(fn) && ctx.__iamLocals.has(fn.text)) fn = unwrap(ctx.__iamLocals.get(fn.text));
983
- if (fn && ts.isPropertyAccessExpression(fn) && ts.isIdentifier(fn.expression)) {
984
- const props = ctx[fn.expression.text], handler = props && typeof props === "object" && forwarded.get(props)?.handlers.find(h => h.key === fn.name.text);
985
- if (handler) return handler;
1129
+ const eventValue = (variableKey) => ({ variableKey, value: {op:'event',key:'value'}, payload:{value:ui.variables.find(v=>v.variableKey===variableKey)?.valueType} });
1130
+ if(fn&&ts.isIdentifier(fn)&&ctx.__iamPropHandlers.has(fn.text))return ctx.__iamPropHandlers.get(fn.text);
1131
+ if(fn&&ts.isIdentifier(fn)&&ctx.__iamSetters[fn.text]&&['onValueChange','onOpenChange'].includes(property)) {
1132
+ const key=ctx.__iamVariableKeys[ctx.__iamSetters[fn.text]];return key?eventValue(key):null;
1133
+ }
1134
+ if(fn&&ts.isIdentifier(fn)&&ctx.__iamLocals.has(fn.text))fn=unwrap(ctx.__iamLocals.get(fn.text));
1135
+ if(fn&&ts.isPropertyAccessExpression(fn)&&ts.isIdentifier(fn.expression)) {
1136
+ const props=ctx[fn.expression.text],handler=props&&typeof props==='object'&&forwarded.get(props)?.handlers.find(h=>h.key===fn.name.text);
1137
+ if(handler)return handler;
1138
+ }
1139
+ if(!fn||!ts.isArrowFunction(fn)||fn.modifiers?.some(m=>m.kind===ts.SyntaxKind.AsyncKeyword))return null;
1140
+ let statements=ts.isBlock(fn.body)?[...fn.body.statements]:[fn.body];
1141
+ if(statements.length>16)return null;
1142
+ const writes=[],payload={};
1143
+ const inputs=e=>{if(e.op==='event')payload[e.key]=e.key==='checked'?'boolean':'string';for(const c of [e.arg,e.left,e.right,...(e.args||[]),...(e.parts||[]).map(p=>p.when)])if(c)inputs(c);};
1144
+ const replaceEvent=(value,arg)=>value.op==='event'&&value.key==='value'?arg:Object.fromEntries(Object.entries(value).map(([k,v])=>[k,Array.isArray(v)?v.map(item=>item&&typeof item==='object'?replaceEvent(item,arg):item):v&&typeof v==='object'?replaceEvent(v,arg):v]));
1145
+ for(let body of statements) {
1146
+ if(ts.isExpressionStatement(body)||ts.isReturnStatement(body))body=body.expression;
1147
+ body=unwrap(body);
1148
+ if(body&&ts.isCallExpression(body)&&!body.arguments.length&&ts.isPropertyAccessExpression(body.expression)&&ts.isIdentifier(body.expression.expression)&&body.expression.expression.text===fn.parameters[0]?.name?.text&&['preventDefault','stopPropagation'].includes(body.expression.name.text))continue;
1149
+ if(body&&ts.isBinaryExpression(body)&&body.operatorToken.kind===ts.SyntaxKind.AmpersandAmpersandToken) {
1150
+ const guard=unwrap(body.left);
1151
+ if(!ts.isBinaryExpression(guard)||guard.operatorToken.kind!==ts.SyntaxKind.EqualsEqualsEqualsToken||!ts.isTypeOfExpression(guard.left)||!ts.isIdentifier(guard.left.expression)||guard.left.expression.text!==fn.parameters[0]?.name?.text||!ts.isStringLiteral(guard.right)||!['string','number','boolean'].includes(guard.right.text))return null;
1152
+ body=unwrap(body.right);
1153
+ }
1154
+ if(!body||!ts.isCallExpression(body)||!ts.isIdentifier(body.expression)||body.arguments.length!==1)return null;
1155
+ const inherited=ctx.__iamPropHandlers.get(body.expression.text);
1156
+ if(inherited) {
1157
+ const arg=uiExpression(body.arguments[0],owner,ctx,fn.parameters[0]?.name?.text||null);
1158
+ if(!arg)return null;
1159
+ const forwardedWrites=(inherited.writes||[{variableKey:inherited.variableKey,value:inherited.value}]).map(write=>({...write,value:replaceEvent(write.value,arg)}));
1160
+ for(const write of forwardedWrites)inputs(write.value);
1161
+ if(['onValueChange','onOpenChange'].includes(property))for(const [key,type]of Object.entries(inherited.payload))if(Object.hasOwn(payload,key))payload[key]=type;
1162
+ if(forwardedWrites.some(write=>uiExpressionType(write.value,payload)!==ui.variables.find(v=>v.variableKey===write.variableKey)?.valueType))return null;
1163
+ writes.push(...forwardedWrites);continue;
1164
+ }
1165
+ const label=ctx.__iamSetters[body.expression.text],variableKey=ctx.__iamVariableKeys[label];
1166
+ if(!variableKey||writes.some(w=>w.variableKey===variableKey))return null;
1167
+ const input=unwrap(body.arguments[0]),functional=input&&ts.isArrowFunction(input)&&!ts.isBlock(input.body)&&input.parameters.length===1&&ts.isIdentifier(input.parameters[0].name)?input:null;
1168
+ const aliases=functional?{[functional.parameters[0].name.text]:{op:'var',key:variableKey}}:{};
1169
+ const value=uiExpression(functional?functional.body:input,owner,ctx,fn.parameters[0]?.name?.text||null,aliases);
1170
+ if(!value)return null;
1171
+ inputs(value);const expected=ui.variables.find(v=>v.variableKey===variableKey)?.valueType;
1172
+ if(value.op==='event'&&value.key==='value'&&['onValueChange','onOpenChange'].includes(property))payload.value=expected;
1173
+ if(uiExpressionType(value,payload)!==expected)return null;
1174
+ writes.push({variableKey,value});
1175
+ }
1176
+ if(!writes.length||new Set(writes.map(w=>w.variableKey)).size!==writes.length)return null;
1177
+ return {...writes[0],writes,payload};
1178
+ }
1179
+ function queryBacked(node, owner, ctx) {
1180
+ return sourceQueryEvidence({
1181
+ ts,
1182
+ node,
1183
+ module: owner.module,
1184
+ locals: ctx.__iamLocals,
1185
+ binding,
1186
+ functionFrom,
1187
+ imported: externalImport,
1188
+ });
1189
+ }
1190
+ function remoteResultCondition(node, owner, ctx) {
1191
+ let shape = false;
1192
+ const root =
1193
+ ts.isIdentifier(node) && ctx.__iamLocals.has(node.text)
1194
+ ? ctx.__iamLocals.get(node.text)
1195
+ : node;
1196
+ const inspect = (n) => {
1197
+ if (!n || shape) return;
1198
+ if (
1199
+ ts.isPropertyAccessExpression(n) &&
1200
+ [
1201
+ "isLoading",
1202
+ "isPending",
1203
+ "isError",
1204
+ "error",
1205
+ "data",
1206
+ "length",
1207
+ "size",
1208
+ ].includes(n.name.text)
1209
+ )
1210
+ shape = true;
1211
+ ts.forEachChild(n, inspect);
1212
+ };
1213
+ inspect(root);
1214
+ return shape
1215
+ ? queryBacked(node, owner, ctx)
1216
+ ? "remote"
1217
+ : "unobserved-data"
1218
+ : null;
1219
+ }
1220
+ function updatedCollectionContext(ctx, label, value, owner) {
1221
+ const next = { ...ctx, [label]: value, __iamCollectionTemplate: true };
1222
+ for (const [key, init] of ctx.__iamLocals)
1223
+ if (!ctx.__iamStates.has(key))
1224
+ next[key] = evalExpression(init, owner.module, next);
1225
+ return next;
1226
+ }
1227
+ function finiteCollection(node, owner, ctx, instance, depth) {
1228
+ if (ctx.__iamCollectionTemplate || !ts.isConditionalExpression(node))
1229
+ return null;
1230
+ const maps = [];
1231
+ const scan = (n) => {
1232
+ if (
1233
+ ts.isCallExpression(n) &&
1234
+ ts.isPropertyAccessExpression(n.expression) &&
1235
+ n.expression.name.text === "map"
1236
+ )
1237
+ maps.push(n);
1238
+ if (!ts.isFunctionLike(n)) ts.forEachChild(n, scan);
1239
+ };
1240
+ scan(node);
1241
+ if (maps.length !== 1) return null;
1242
+ const receiver = maps[0].expression.expression,
1243
+ reads = collectionStateReads(ts, receiver, ctx),
1244
+ tabs = presentation.tabs.filter(
1245
+ (t) =>
1246
+ reads.includes(t.variableKey) &&
1247
+ Object.values(ctx.__iamVariableKeys).includes(t.variableKey),
1248
+ );
1249
+ if (tabs.length !== 1 || tabs[0].items.length < 2 || tabs[0].items.length > 8)
1250
+ return null;
1251
+ const tab = tabs[0],
1252
+ label = Object.keys(ctx.__iamVariableKeys).find(
1253
+ (k) => ctx.__iamVariableKeys[k] === tab.variableKey,
1254
+ );
1255
+ const candidates = tab.items.map((item) => {
1256
+ const next = updatedCollectionContext(ctx, label, item.value, owner);
1257
+ return {
1258
+ value: item.value,
1259
+ ctx: next,
1260
+ items: evalExpression(receiver, owner.module, next),
1261
+ };
1262
+ });
1263
+ if (
1264
+ candidates.some((c) => !Array.isArray(c.items) || c.items.length > 128) ||
1265
+ new Set(candidates.map((c) => JSON.stringify(c.items))).size < 2
1266
+ )
1267
+ return null;
1268
+ const source = sourceLocation(owner.module, node, owner.symbol),
1269
+ captured = {};
1270
+ const capture = (n) => {
1271
+ if (
1272
+ ts.isIdentifier(n) &&
1273
+ Object.hasOwn(ctx, n.text) &&
1274
+ !n.text.startsWith("__iam")
1275
+ ) {
1276
+ const value = ctx[n.text];
1277
+ captured[n.text] = isUnknown(value)
1278
+ ? { kind: "unknown" }
1279
+ : value === undefined
1280
+ ? { kind: "undefined" }
1281
+ : value?.[COMPONENT]
1282
+ ? { kind: "component", key: value[COMPONENT].key }
1283
+ : value?.[LUCIDE_SOURCE]
1284
+ ? { kind: "lucide", name: value[LUCIDE_SOURCE] }
1285
+ : { kind: "literal", value };
1286
+ }
1287
+ ts.forEachChild(n, capture);
1288
+ };
1289
+ capture(node);
1290
+ let cacheKey;
1291
+ try {
1292
+ cacheKey = identity("collection-template", {
1293
+ source,
1294
+ captured,
1295
+ cases: candidates.map((c) => ({ value: c.value, items: c.items })),
1296
+ });
1297
+ } catch {
1298
+ return null;
1299
+ }
1300
+ let cases = collectionTemplates.get(cacheKey);
1301
+ if (!cases) {
1302
+ cases = [];
1303
+ for (const candidate of candidates) {
1304
+ const sizes = {
1305
+ bindings: ui.bindings.length,
1306
+ transitions: ui.transitions.length,
1307
+ variables: ui.variables.length,
1308
+ conditions: presentation.conditions.length,
1309
+ tabs: presentation.tabs.length,
1310
+ regions: presentation.regions.length,
1311
+ };
1312
+ const html = renderExpr(
1313
+ node,
1314
+ owner,
1315
+ candidate.ctx,
1316
+ cacheKey + "/" + candidate.value,
1317
+ // A shared source fragment owns its bounded subtree budget. App wrapper
1318
+ // depth must not erase leaf text from an otherwise identical collection.
1319
+ 0,
1320
+ ),
1321
+ fragment = sourceCollectionFragment(
1322
+ html,
1323
+ source,
1324
+ candidate.items.length,
1325
+ record,
1326
+ );
1327
+ for (const variable of ui.variables.slice(sizes.variables))
1328
+ uiVariableKeys.delete(variable.variableKey);
1329
+ ui.bindings.length = sizes.bindings;
1330
+ ui.transitions.length = sizes.transitions;
1331
+ ui.variables.length = sizes.variables;
1332
+ for (const key of ["conditions", "tabs", "regions"])
1333
+ presentation[key].length = sizes[key];
1334
+ if (
1335
+ !presentation.fragments.some(
1336
+ (f) => f.fragmentKey === fragment.fragmentKey,
1337
+ )
1338
+ )
1339
+ presentation.fragments.push(fragment);
1340
+ cases.push({ value: candidate.value, fragmentKey: fragment.fragmentKey });
1341
+ }
1342
+ collectionTemplates.set(cacheKey, cases);
1343
+ }
1344
+ const collectionKey = identity("ui-collection", {
1345
+ page: pageKey,
1346
+ instance,
1347
+ source,
1348
+ }),
1349
+ nodeId = identity("node", { page: pageKey, collectionKey }),
1350
+ elementKey = identity("element", {
1351
+ component: owner.key,
1352
+ sourceCollection: node.pos,
1353
+ });
1354
+ elements.set(elementKey, {
1355
+ elementKey,
1356
+ componentKey: owner.key,
1357
+ tag: "div",
1358
+ source,
1359
+ identityStrategy: "provisional_ast_ordinal",
1360
+ editableProperties: [],
1361
+ binding: "read_only",
1362
+ });
1363
+ record.set(nodeId, { elementKey, source, synthetic: true });
1364
+ const requires = reads
1365
+ .filter((key) => key !== tab.variableKey)
1366
+ .map((variableKey) => ({
1367
+ variableKey,
1368
+ value: ui.variables.find((v) => v.variableKey === variableKey)
1369
+ ?.initialValue,
1370
+ }));
1371
+ presentation.collections.push({
1372
+ collectionKey,
1373
+ routeKey: pageKey,
1374
+ renderKey: "pending",
1375
+ nodeId,
1376
+ variableKey: tab.variableKey,
1377
+ requires,
1378
+ cases,
1379
+ source,
1380
+ });
1381
+ return (
1382
+ '<div data-iam-node="' +
1383
+ nodeId +
1384
+ '" data-iam-collection="' +
1385
+ collectionKey +
1386
+ '" data-iam-unobserved="true" hidden=""></div>'
1387
+ );
1388
+ }
1389
+ const iconGlyphs=new Set();
1390
+ function renderLucide(icon, node, owner, props, instance, shared=true) {
1391
+ const geometry = lucideGeometry(icon),
1392
+ source = sourceLocation(owner.module, node, owner.symbol);
1393
+ if (!geometry) return "";
1394
+ const element = (tag, attributes, children, part) => {
1395
+ const elementKey = identity("element", {
1396
+ component: owner.key,
1397
+ lucide: node.pos,
1398
+ part,
1399
+ tag,
1400
+ }),
1401
+ nodeId = identity("node", {
1402
+ page: pageKey,
1403
+ elementKey,
1404
+ instance,
1405
+ icon: icon[LUCIDE_SOURCE],
1406
+ });
1407
+ elements.set(elementKey, {
1408
+ elementKey,
1409
+ componentKey: owner.key,
1410
+ tag,
1411
+ source,
1412
+ identityStrategy: "provisional_ast_ordinal",
1413
+ editableProperties: [],
1414
+ binding: "read_only",
1415
+ });
1416
+ record.set(nodeId, { elementKey, source, synthetic: true });
1417
+ return (
1418
+ "<" +
1419
+ tag +
1420
+ ' data-iam-node="' +
1421
+ nodeId +
1422
+ '" ' +
1423
+ Object.entries(attributes)
1424
+ .map(([key, value]) => key + '="' + escape(value) + '"')
1425
+ .join(" ") +
1426
+ ">" +
1427
+ children +
1428
+ "</" +
1429
+ tag +
1430
+ ">"
1431
+ );
1432
+ };
1433
+ const className = typeof props.className === "string" ? props.className : "";
1434
+ className
1435
+ .split(/\s+/)
1436
+ .filter(Boolean)
1437
+ .forEach((c) => allClasses.add(c));
1438
+ const glyphId='iam-glyph-'+sha256(String(icon[LUCIDE_SOURCE])).slice(0,24);
1439
+ let glyph='';if(shared&&!iconGlyphs.has(glyphId)){
1440
+ iconGlyphs.add(glyphId);glyph=element('defs',{},element('g',{id:glyphId},geometry.map(([tag,attrs],index)=>element(tag,attrs,'','glyph-'+glyphId+'-'+index)).join(''),'glyph-'+glyphId),'defs-'+glyphId);
986
1441
  }
987
- if (!fn || !ts.isArrowFunction(fn) || fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
988
- const body = ts.isBlock(fn.body) ? fn.body.statements.length === 1 && ts.isExpressionStatement(fn.body.statements[0]) ? fn.body.statements[0].expression : null : unwrap(fn.body);
989
- if (!body || !ts.isCallExpression(body) || !ts.isIdentifier(body.expression) || body.arguments.length !== 1) return null;
990
- const label = ctx.__iamSetters[body.expression.text], variableKey = ctx.__iamVariableKeys[label];
991
- if (!variableKey) return null;
992
- const input = unwrap(body.arguments[0]);
993
- const functional = input && ts.isArrowFunction(input) && !ts.isBlock(input.body) && input.parameters.length === 1 && ts.isIdentifier(input.parameters[0].name) ? input : null;
994
- const aliases = functional ? { [functional.parameters[0].name.text]: { op: "var", key: variableKey } } : {};
995
- const value = uiExpression(functional ? functional.body : input, owner, ctx, fn.parameters[0]?.name?.text || null, aliases);
996
- if (!value) return null;
997
- const payload = {};
998
- function eventInputs(e) { if (e.op === "event") payload[e.key] = e.key === "checked" ? "boolean" : "string"; for (const child of [e.arg, e.left, e.right, ...(e.args || []),...(e.parts||[]).map(part=>part.when)]) if (child) eventInputs(child); }
999
- eventInputs(value);
1000
- if (uiExpressionType(value, payload) !== ui.variables.find(variable => variable.variableKey === variableKey)?.valueType) return null;
1001
- return { variableKey, value, payload };
1442
+ return element(
1443
+ "svg",
1444
+ {
1445
+ xmlns: "http://www.w3.org/2000/svg",
1446
+ width: 24,
1447
+ height: 24,
1448
+ viewBox: "0 0 24 24",
1449
+ fill: "none",
1450
+ stroke: "currentColor",
1451
+ "stroke-width":
1452
+ typeof props.strokeWidth === "number" ? props.strokeWidth : 2,
1453
+ "stroke-linecap": "round",
1454
+ "stroke-linejoin": "round",
1455
+ "aria-hidden": "true",
1456
+ class: className,
1457
+ },
1458
+ shared?glyph+element('use',{href:'#'+glyphId},'','use'):geometry.map(([tag,attrs],index)=>element(tag,attrs,'',index)).join(''),
1459
+ "root",
1460
+ );
1002
1461
  }
1003
- function unobservedValue(node, owner, instance) {
1462
+ function unobservedValue(node, owner, instance,remote=false) {
1004
1463
  const source = sourceLocation(owner.module, node, owner.symbol);
1005
1464
  const elementKey = identity("element", { component: owner.key, unobservedValue: node.getStart(owner.module.source) - owner.fn.getStart(owner.module.source) });
1006
1465
  const nodeId = identity("node", { page: pageKey, element: elementKey, instance });
1007
1466
  elements.set(elementKey, { elementKey, componentKey: owner.key, tag: "span", source, identityStrategy: "provisional_ast_ordinal", editableProperties: [], binding: "preview_only" });
1008
1467
  record.set(nodeId, { elementKey, source, synthetic: true });
1009
1468
  // Preserve an unknown child boundary. An empty projection must never prove that replacing text is safe.
1010
- return '<span data-iam-node="' + nodeId + '" hidden=""></span>';
1469
+ return '<span data-iam-node="' + nodeId + '" data-iam-observation="'+(remote?'remote':'unobserved')+'" hidden=""></span>';
1011
1470
  }
1471
+ function renderInvocation(invocation,instance,depth){
1472
+ const {callback,args,caller}=invocation,next={...callback.ctx,__iamContexts:caller.__iamContexts??callback.ctx.__iamContexts,__iamOverlays:caller.__iamOverlays??callback.ctx.__iamOverlays};
1473
+ for(const [index,parameter]of callback.fn.parameters.entries()){
1474
+ const value=args[index];
1475
+ if(ts.isIdentifier(parameter.name))next[parameter.name.text]=value;
1476
+ else if(ts.isObjectBindingPattern(parameter.name))for(const field of parameter.name.elements)if(ts.isIdentifier(field.name))next[field.name.text]=!isUnknown(value)&&value&&typeof value==='object'?value[field.propertyName?.text||field.name.text]??UNKNOWN:UNKNOWN;
1477
+ }
1478
+ gap('RENDER_PROP_SOURCE_ONLY',callback.owner.module.file,callback.owner.symbol+': invoked JSX render-prop reconstructed from source; effects and business callbacks are not executed');
1479
+ return renderExpr(uiReturnExpression(callback.fn),callback.owner,next,callback.instance+'/call-'+instance,depth+1);
1480
+ }
1481
+ function hasSourceJsx(node){let found=false;const visit=n=>{if(ts.isJsxElement(n)||ts.isJsxSelfClosingElement(n)||ts.isJsxFragment(n)){found=true;return;}if(!ts.isFunctionLike(n))ts.forEachChild(n,visit);};if(node)visit(node);return found;}
1012
1482
  function renderExpr(node, owner, ctx, instance, depth) {
1013
1483
  node = unwrap(node);
1014
1484
  if (!node) return "";
1015
1485
  if (depth > 45) { gap("RENDER_DEPTH_LIMIT", owner.module.file, owner.symbol + ": JSX traversal budget exceeded"); return ""; }
1016
1486
  const m = owner.module;
1487
+ const collection=finiteCollection(node,owner,ctx,instance,depth);if(collection!==null)return collection;
1488
+ if(ts.isIdentifier(node)&&ctx.__iamLocals.has(node.text)&&hasSourceJsx(ctx.__iamLocals.get(node.text))){
1489
+ if(ctx.__iamActiveJsx?.has(node.text))return '';
1490
+ return renderExpr(ctx.__iamLocals.get(node.text),owner,{...ctx,__iamActiveJsx:new Set([...(ctx.__iamActiveJsx??[]),node.text])},instance+'/local-'+node.text,depth+1);
1491
+ }
1017
1492
  if (ts.isJsxFragment(node))
1018
1493
  return node.children
1019
1494
  .map((n, i) =>
@@ -1023,6 +1498,13 @@ export async function analyze({
1023
1498
  if (ts.isJsxText(node)) return escape(jsxText(node.text));
1024
1499
  if (ts.isJsxExpression(node))
1025
1500
  return renderExpr(node.expression, owner, ctx, instance, depth + 1);
1501
+ if (ts.isBinaryExpression(node) && [ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.BarBarToken].includes(node.operatorToken.kind)) {
1502
+ const left = evalExpression(node.left, m, ctx);
1503
+ if (!isUnknown(left)) {
1504
+ const fallback = node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ? left == null : !left;
1505
+ return renderExpr(fallback ? node.right : node.left, owner, ctx, instance + '/fallback', depth + 1);
1506
+ }
1507
+ }
1026
1508
  if(ts.isArrowFunction(node)){
1027
1509
  const parent=node.parent?.parent,tag=parent&&ts.isJsxElement(parent)?parent.openingElement.tagName.getText(m.source):'',imported=m.imports.get(tag),body=unwrap(node.body);
1028
1510
  if(imported?.specifier==='react-basics'&&imported.name==='Modal'&&!node.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&node.parameters.length<=3&&node.parameters.every(p=>ts.isIdentifier(p.name))&&(ts.isJsxElement(body)||ts.isJsxFragment(body)||ts.isJsxSelfClosingElement(body))){
@@ -1113,12 +1595,14 @@ export async function analyze({
1113
1595
  gap("UNOBSERVED_CONDITION", m.file, label);
1114
1596
  // Preserve both source alternatives for exact runtime binding. Neither
1115
1597
  // is asserted to be authorized/visible, nor executed in source preview.
1598
+ const dataBranches=[yes,no].map(branch=>branch?evalExpression(branch,m,ctx):null),observation=remoteResultCondition(condition,owner,ctx)??(dataBranches.every(v=>v===null||['string','number'].includes(typeof v))?'unobserved-data':null),conditionRow={conditionKey:identity('ui-condition',{file:m.file,offset:condition.getStart(m.source)}),expression:label,source:sourceLocation(m,condition,owner.symbol),...(observation?{observation}:{})};
1599
+ presentation.conditions.push(conditionRow);
1116
1600
  return [yes, no].map((branch,index)=>{
1117
1601
  if(!branch)return '';
1118
1602
  const elementKey=identity('element',{component:owner.key,state:label,unobserved:index,sourceOffset:condition.getStart(m.source)-owner.fn.getStart(m.source)}),id=identity('node',{page:pageKey,element:elementKey,instance:instance+'/unobserved-'+index}),source=sourceLocation(m,condition,owner.symbol);
1119
1603
  record.set(id,{elementKey,source,synthetic:true});
1120
1604
  elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});
1121
- return '<div data-iam-node="'+id+'" data-iam-unobserved="true" hidden="">'+renderExpr(branch,owner,ctx,instance+'/unobserved-'+index,depth+1)+'</div>';
1605
+ return '<div data-iam-node="'+id+'" data-iam-unobserved="true" hidden="" data-iam-condition="'+conditionRow.conditionKey+'" data-iam-alternative="'+(index===0)+'">'+renderExpr(branch,owner,ctx,instance+'/unobserved-'+index,depth+1)+'</div>';
1122
1606
  }).join('');
1123
1607
  }
1124
1608
  if (
@@ -1133,22 +1617,28 @@ export async function analyze({
1133
1617
  fn &&
1134
1618
  (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))
1135
1619
  ) {
1136
- if (values.length > 50)
1620
+ const limit=ctx.__iamCollectionTemplate?128:50;
1621
+ if (values.length > limit)
1137
1622
  gap(
1138
1623
  "COLLECTION_TRUNCATED",
1139
1624
  m.file,
1140
1625
  "Static collection limited to 50 preview items",
1141
1626
  );
1142
1627
  return values
1143
- .slice(0, 50)
1628
+ .slice(0, limit)
1144
1629
  .map((value, i) => {
1145
1630
  const next = { ...ctx };
1146
1631
  if (fn.parameters[0] && ts.isIdentifier(fn.parameters[0].name))
1147
1632
  next[fn.parameters[0].name.text] = value;
1148
1633
  if (fn.parameters[1] && ts.isIdentifier(fn.parameters[1].name))
1149
1634
  next[fn.parameters[1].name.text] = i;
1635
+ if(fn.parameters[0]&&ts.isObjectBindingPattern(fn.parameters[0].name))for(const field of fn.parameters[0].name.elements){
1636
+ if(!ts.isIdentifier(field.name)||field.dotDotDotToken)continue;
1637
+ const key=field.propertyName?.text||field.name.text;next[field.name.text]=value&&typeof value==='object'&&!isUnknown(value)&&Object.hasOwn(value,key)?value[key]:field.initializer?evalExpression(field.initializer,m,next):undefined;
1638
+ }
1639
+ if(ts.isBlock(fn.body))for(const statement of fn.body.statements)if(ts.isVariableStatement(statement))for(const decl of statement.declarationList.declarations)if(ts.isIdentifier(decl.name))next[decl.name.text]=evalExpression(decl.initializer,m,next);
1150
1640
  return renderExpr(
1151
- returnExpression(fn),
1641
+ uiReturnExpression(fn),
1152
1642
  owner,
1153
1643
  next,
1154
1644
  instance + "/item-" + i,
@@ -1158,7 +1648,7 @@ export async function analyze({
1158
1648
  .join("");
1159
1649
  }
1160
1650
  gap("DYNAMIC_COLLECTION", m.file, node.expression.getText(m.source));
1161
- return unobservedValue(node, owner, instance);
1651
+ return unobservedValue(node, owner, instance,queryBacked(node.expression.expression,owner,ctx));
1162
1652
  }
1163
1653
  if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) {
1164
1654
  if (
@@ -1168,6 +1658,9 @@ export async function analyze({
1168
1658
  )
1169
1659
  return ctx[node.text].iamChildren;
1170
1660
  const value = evalExpression(node, m, ctx), expr = uiExpression(node, owner, ctx);
1661
+ if(value?.iamRenderInvocation)return renderInvocation(value.iamRenderInvocation,instance,depth);
1662
+ if(value?.iamRenderChildren)return value.iamRenderChildren(ctx);
1663
+ if(value?.iamChildren!==undefined)return value.iamChildren;
1171
1664
  if (expr && readVariables(expr).length && ["string", "number"].includes(typeof value)) {
1172
1665
  const source = sourceLocation(m, node, owner.symbol), elementKey = identity("element", { component: owner.key, stateTextOffset: node.getStart(m.source) - owner.fn.getStart(m.source) }), id = identity("node", { page: pageKey, element: elementKey, instance });
1173
1666
  record.set(id, { elementKey, source, synthetic: true });
@@ -1184,7 +1677,7 @@ export async function analyze({
1184
1677
  return escape(value);
1185
1678
  if (isUnknown(value)) {
1186
1679
  gap("DYNAMIC_VALUE", m.file, node.getText(m.source).slice(0, 100));
1187
- return unobservedValue(node, owner, instance);
1680
+ return unobservedValue(node, owner, instance,queryBacked(node,owner,ctx));
1188
1681
  }
1189
1682
  return "";
1190
1683
  }
@@ -1192,12 +1685,13 @@ export async function analyze({
1192
1685
  throw new Error("Rendered node budget exceeded");
1193
1686
  const opening = ts.isJsxElement(node) ? node.openingElement : node,
1194
1687
  tag = opening.tagName.getText(m.source),
1195
- props = m.anchors.get(opening) ? { "data-indiana-id": m.anchors.get(opening) } : {}, uiHandlers = [], uiBindings = [], forwardedHandlers=[];
1688
+ props = m.anchors.get(opening) ? { "data-indiana-id": m.anchors.get(opening) } : {}, uiHandlers = [], uiBindings = [], forwardedHandlers=[],uiAttributes=[];
1196
1689
  for (const attr of opening.attributes.properties) {
1197
1690
  if (ts.isJsxSpreadAttribute(attr)) {
1198
1691
  const v = evalExpression(attr.expression, m, ctx);
1199
1692
  if (isUnknown(v)) {
1200
1693
  for (const attribute of anchorAttributes) delete props[attribute];
1694
+ props.__iamOpaqueProps = true;
1201
1695
  gap("UNKNOWN_ANCHOR_SPREAD", m.file, "Opaque spread may override element identity");
1202
1696
  continue;
1203
1697
  }
@@ -1207,17 +1701,22 @@ export async function analyze({
1207
1701
  for (const key of Object.keys(v)) {
1208
1702
  for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
1209
1703
  for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
1704
+ for (let i = uiAttributes.length - 1; i >= 0; i--) if (uiAttributes[i].key === key) uiAttributes.splice(i, 1);
1210
1705
  }
1211
- if (inherited) { uiBindings.push(...inherited.bindings); uiHandlers.push(...inherited.handlers); }
1706
+ if (inherited) { uiBindings.push(...inherited.bindings); uiHandlers.push(...inherited.handlers); uiAttributes.push(...(inherited.attributes??[])); }
1212
1707
  }
1213
1708
  continue;
1214
1709
  }
1215
1710
  const key = attr.name.getText(m.source);
1711
+ for(let i=uiAttributes.length-1;i>=0;i--)if(uiAttributes[i].key===key)uiAttributes.splice(i,1);
1712
+ if(!ctx.__iamCollectionTemplate&&['className','placeholder','aria-label'].includes(key)&&attr.initializer&&ts.isJsxExpression(attr.initializer)){
1713
+ const reads=collectionStateReads(ts,attr.initializer.expression,ctx);if(reads.length)uiAttributes.push({key,owner,ctx,expression:attr.initializer.expression,property:key==='className'?'class':key,reads,source:sourceLocation(m,attr,owner.symbol)});
1714
+ }
1216
1715
  for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
1217
1716
  for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
1218
- if (["onClick", "onChange", "onInput"].includes(key)) {
1717
+ if (["onClick", "onChange", "onInput", "onValueChange", "onOpenChange"].includes(key)) {
1219
1718
  props[key] = undefined;
1220
- const supported = uiHandler(attr.initializer, owner, ctx);
1719
+ const supported = ctx.__iamCollectionTemplate?null:uiHandler(attr.initializer, owner, ctx, key);
1221
1720
  if (supported) { props[key] = undefined; uiHandlers.push({ ...supported, key, source: sourceLocation(m, attr, owner.symbol) }); }
1222
1721
  const fn =
1223
1722
  attr.initializer && ts.isJsxExpression(attr.initializer)
@@ -1249,7 +1748,7 @@ export async function analyze({
1249
1748
  key +
1250
1749
  " at line " +
1251
1750
  sourceLocation(m, attr, owner.symbol).line +
1252
- ": callback is not a supported UI simulation; business/data behavior is not executed",
1751
+ ": handler is not simulated; no business execution",
1253
1752
  );
1254
1753
  continue;
1255
1754
  }
@@ -1275,6 +1774,16 @@ export async function analyze({
1275
1774
  const expr = uiExpression(attr.initializer.expression, owner, ctx);
1276
1775
  if (expr && readVariables(expr).length) uiBindings.push({ property: key, value: expr, source: sourceLocation(m, attr, owner.symbol) });
1277
1776
  }
1777
+ if(attr.initializer&&ts.isJsxExpression(attr.initializer)){
1778
+ let fn=unwrap(attr.initializer.expression);const visited=new Set();
1779
+ while(fn&&ts.isIdentifier(fn)&&ctx.__iamLocals.has(fn.text)&&!visited.has(fn.text)){visited.add(fn.text);fn=unwrap(ctx.__iamLocals.get(fn.text));}
1780
+ if(fn&&(ts.isArrowFunction(fn)||ts.isFunctionExpression(fn))){
1781
+ let jsx=false;const inspect=n=>{if(ts.isJsxElement(n)||ts.isJsxSelfClosingElement(n)||ts.isJsxFragment(n))jsx=true;ts.forEachChild(n,inspect);};inspect(fn.body);
1782
+ const safe=!fn.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&(!ts.isBlock(fn.body)||fn.body.statements.every(s=>ts.isVariableStatement(s)||ts.isIfStatement(s)||ts.isReturnStatement(s)));
1783
+ if(jsx&&safe){props[key]={iamRenderProp:{fn,owner,ctx,instance:instance+'/prop-'+key}};continue;}
1784
+ }
1785
+ }
1786
+ if(attr.initializer&&ts.isJsxExpression(attr.initializer)&&hasSourceJsx(attr.initializer.expression)){const value=attr.initializer.expression;props[key]={iamRenderChildren:inherited=>renderExpr(value,owner,{...ctx,__iamContexts:inherited.__iamContexts??ctx.__iamContexts,__iamTabs:inherited.__iamTabs??ctx.__iamTabs,__iamOverlays:inherited.__iamOverlays??ctx.__iamOverlays},instance+'/prop-'+key,depth+1)};continue;}
1278
1787
  props[key] = !attr.initializer
1279
1788
  ? true
1280
1789
  : ts.isStringLiteral(attr.initializer)
@@ -1285,24 +1794,96 @@ export async function analyze({
1285
1794
  gap('DYNAMIC_CLASS_PARTIAL',m.file,'className at line '+sourceLocation(m,attr,owner.symbol).line+': only definitely present class tokens retained; unresolved styling needs runtime observation');
1286
1795
  }
1287
1796
  }
1288
- const children = ts.isJsxElement(node)
1289
- ? node.children
1290
- .map((child, i) =>
1291
- renderExpr(
1292
- child,
1293
- owner,
1294
- ctx,
1295
- instance + "/child-" + i,
1296
- depth + 1,
1297
- ),
1298
- )
1299
- .join("")
1300
- : props.children?.iamChildren || "";
1797
+ const chart=sourceChart({ts,node:opening,tag,owner,context:ctx,evaluate:evalExpression,imported:externalImport});
1798
+ if(chart){
1799
+ gap(chart.unobserved?'CHART_DATA_UNOBSERVED':'CHART_SOURCE_PROJECTION',m.file,chart.unobserved?'Chart data is dynamic, random or outside bounds; no values are invented.':'Bounded source data rendered as inert SVG; library effects, animation and tooltips are not executed.');
1800
+ const source=sourceLocation(m,node,owner.symbol);
1801
+ const draw=(item,part)=>{
1802
+ if(typeof item==='string')return escape(item);
1803
+ const elementKey=identity('element',{component:owner.key,chart:node.pos,part}),nodeId=identity('node',{page:pageKey,elementKey,instance});
1804
+ elements.set(elementKey,{elementKey,componentKey:owner.key,tag:item.tag,source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(nodeId,{elementKey,source,synthetic:true});
1805
+ return '<'+item.tag+' data-iam-node="'+nodeId+'" '+Object.entries(item.attrs).map(([k,v])=>k+'="'+escape(v)+'"').join(' ')+'>'+item.children.map((child,i)=>draw(child,part+'/'+i)).join('')+'</'+item.tag+'>';
1806
+ };
1807
+ return draw({tag:'svg',attrs:{xmlns:'http://www.w3.org/2000/svg',viewBox:'0 0 '+chart.width+' '+chart.height,width:chart.width,height:chart.height,role:'img','aria-label':chart.unobserved?'Chart: data unobserved':'Source chart projection','data-iam-chart':chart.unobserved?'unobserved':'source',style:'display:block;width:100%;height:auto;max-height:'+chart.height+'px'},children:chart.children},'root');
1808
+ }
1809
+ const sourceIcon=evalExpression(opening.tagName,m,ctx);
1810
+ if(sourceIcon?.[LUCIDE_SOURCE])return renderLucide(sourceIcon,node,owner,props,instance,!ctx.__iamCollectionTemplate);
1811
+ const tabPart=tabsPrimitive(m,tag),basePart=basePrimitive(m,tag),overlay=basePart&&basePart.family!=='Tabs'?basePart:null;
1812
+ let overlayGroup=overlay?ctx.__iamOverlays?.get(overlay.family):null;
1813
+ let childCtx=ctx,tabGroup=ctx.__iamTabs;
1814
+ const provider=tag.endsWith('.Provider')?contextKey(m,tag.slice(0,-9)):contextKey(m,tag);
1815
+ if(provider){const contexts=new Map(ctx.__iamContexts);contexts.set(provider,props.value);childCtx={...childCtx,__iamContexts:contexts};}
1816
+ if(overlay&&['Root','SubmenuRoot'].includes(overlay.part)){
1817
+ const openProperty=overlay.family==='Transition'?'in':'open',open=props[openProperty];
1818
+ overlayGroup={open:open===true||open===undefined&&props.defaultOpen===true,variable:uiBindings.find(b=>b.property===openProperty)?.value,controller:uiHandlers.find(h=>h.key==='onOpenChange')};
1819
+ if(overlay.family!=='Transition'&&open===undefined&&!props.__iamOpaqueProps&&(props.defaultOpen===undefined||typeof props.defaultOpen==='boolean')){
1820
+ const scopeKey=identity('primitive-state',{page:pageKey,instance,family:overlay.family}),variableKey=identity('ui-variable',{scope:scopeKey,label:'open'});
1821
+ ui.variables.push({variableKey,routeKey:pageKey,scopeKey,label:overlay.family+'.open',valueType:'boolean',initialValue:overlayGroup.open,source:sourceLocation(m,node,owner.symbol)});
1822
+ overlayGroup.variable={op:'var',key:variableKey};
1823
+ overlayGroup.controller={variableKey,value:{op:'event',key:'value'},payload:{value:'boolean'}};
1824
+ }
1825
+ if(overlay.family==='Transition'){
1826
+ const attr=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text==='onEnter');
1827
+ const callback=attr?.initializer&&ts.isJsxExpression(attr.initializer)?unwrap(attr.initializer.expression):null;
1828
+ let statement=callback&&ts.isArrowFunction(callback)&&!callback.parameters.length&&ts.isBlock(callback.body)?callback.body.statements[0]:null;
1829
+ if(statement&&ts.isIfStatement(statement))statement=evalExpression(statement.expression,m,ctx)===true?statement.thenStatement:null;
1830
+ if(statement&&ts.isBlock(statement))statement=statement.statements[0];
1831
+ const assignment=statement&&ts.isExpressionStatement(statement)?unwrap(statement.expression):null;
1832
+ const left=assignment&&ts.isBinaryExpression(assignment)&&assignment.operatorToken.kind===ts.SyntaxKind.EqualsToken?assignment.left:null;
1833
+ if(left&&ts.isPropertyAccessExpression(left)&&left.name.text==='display'&&ts.isPropertyAccessExpression(left.expression)&&left.expression.name.text==='style'&&ts.isPropertyAccessExpression(left.expression.expression)&&left.expression.expression.name.text==='current'&&ts.isIdentifier(left.expression.expression.expression)&&(assignment.right.kind===ts.SyntaxKind.NullKeyword||ts.isStringLiteral(assignment.right)&&assignment.right.text==='')){
1834
+ overlayGroup.reveal={ref:left.expression.expression.expression.text,file:m.file,symbol:owner.symbol};
1835
+ gap('TRANSITION_VISIBILITY_SOURCE_ONLY',m.file,'Source onEnter clears the referenced child display style; only that UI assignment is projected, without running callbacks or effects.');
1836
+ }
1837
+ }
1838
+ if(isUnknown(open)){
1839
+ const attribute=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text===openProperty),expression=attribute?.initializer&&ts.isJsxExpression(attribute.initializer)?attribute.initializer.expression:opening;
1840
+ overlayGroup.condition={conditionKey:identity('ui-condition',{file:m.file,offset:expression.getStart(m.source)}),expression:attribute?redactLiteral(expression.getText(m.source)).slice(0,160):'open (forwarded)',source:sourceLocation(m,expression,owner.symbol)};
1841
+ presentation.conditions.push(overlayGroup.condition);
1842
+ }
1843
+ const overlays=new Map(ctx.__iamOverlays);overlays.set(overlay.family,overlayGroup);childCtx={...childCtx,__iamOverlays:overlays};
1844
+ }
1845
+ if(tabPart==='Root') {
1846
+ const valueBinding=uiBindings.find(b=>b.property==='value')?.value;
1847
+ const controller=uiHandlers.find(h=>h.key==='onValueChange');
1848
+ tabGroup={tabKey:identity('ui-tab',{page:pageKey,scope:ctx.__iamScope,instance}),routeKey:pageKey,renderKey:'pending',nodeId:'pending',variableKey:valueBinding?.op==='var'?valueBinding.key:null,initialValue:typeof props.value==='string'?props.value:typeof props.defaultValue==='string'?props.defaultValue:null,items:[],source:sourceLocation(m,node,owner.symbol),scopeSource:ui.variables.find(v=>v.variableKey===valueBinding?.key)?.source??sourceLocation(m,node,owner.symbol),controller};
1849
+ childCtx={...childCtx,__iamTabs:tabGroup};
1850
+ }
1851
+ const localRef=componentBinding(m,tag),localTarget=ctx[tag]?.[COMPONENT]??(localRef&&componentDefs.get(localRef.module.file+'#'+localRef.name));
1852
+ const dynamicNode=ctx.__iamLocals.get(tag);
1853
+ const registry=!localTarget&&dynamicNode?componentRegistry({ts,node:dynamicNode,module:m,locals:ctx.__iamLocals,binding,functionFrom,value:(n,module)=>evalExpression(n,module,ctx),component:(n,module)=>sourceComponent(n,module)?.[COMPONENT],variable:name=>ctx.__iamVariableKeys[name]??(ctx.__iamPropBindings.get(name)?.op==='var'?ctx.__iamPropBindings.get(name).key:null),location:sourceLocation,unknown:UNKNOWN,imported:externalImport}):null;
1854
+ const renderChildren=(inherited=childCtx)=>ts.isJsxElement(node)?node.children.map((child,i)=>renderExpr(child,owner,{...childCtx,__iamContexts:inherited.__iamContexts??childCtx.__iamContexts,__iamTabs:inherited.__iamTabs??childCtx.__iamTabs,__iamOverlays:inherited.__iamOverlays??childCtx.__iamOverlays},instance+'/child-'+i,depth+1)).join(''):props.children?.iamRenderChildren?props.children.iamRenderChildren(inherited):props.children?.iamChildren||'';
1855
+ const children = localTarget||registry ? '' : renderChildren();
1856
+
1857
+ // Project the documented Controller render callback from source, with no
1858
+ // form initialization, live field values or business callback execution.
1859
+ const formPrimitive = externalImport(m, tag);
1860
+ if (formPrimitive?.specifier === 'react-hook-form') {
1861
+ if (formPrimitive.name === 'FormProvider') return children;
1862
+ if (formPrimitive.name === 'Controller' && props.render?.iamRenderProp)
1863
+ return renderInvocation({ callback: props.render.iamRenderProp,
1864
+ args: [{ field: { name: typeof props.name === 'string' ? props.name : UNKNOWN }, fieldState: UNKNOWN, formState: UNKNOWN }],
1865
+ caller: childCtx }, instance + '/controller', depth + 1);
1866
+ }
1867
+ const slotPrimitive = ctx[tag]?.[UI_PRIMITIVE]??externalImport(m, tag);
1868
+ if (slotPrimitive?.specifier === '@radix-ui/react-slot' && ['Slot', 'Root'].includes(slotPrimitive.name)) {
1869
+ const fragment=JSDOM.fragment(children),only=[...fragment.childNodes].filter(n=>n.nodeType!==3||n.textContent.trim());
1870
+ if(only.length!==1||only[0].nodeType!==1){gap('SLOT_CHILD_UNSUPPORTED',m.file,'Slot requires one concrete element; source-only children retained without prop merging');return children;}
1871
+ const child=only[0];
1872
+ if(typeof props.className==='string'){
1873
+ const names=[props.className,child.getAttribute('class')].filter(Boolean).join(' ');child.setAttribute('class',names);names.split(/\s+/).filter(Boolean).forEach(c=>allClasses.add(c));
1874
+ }
1875
+ for(const [key,value]of Object.entries(props))if(/^(?:title|role|data-(?:slot|state|side|variant|collapsible|active)|aria-[a-z-]+)$/.test(key)&&['string','number','boolean'].includes(typeof value)&&!child.hasAttribute(key))child.setAttribute(key,String(value));
1876
+ if(props.style&&typeof props.style==='object'){const css=staticStyle(props.style,()=>gap('INLINE_STYLE_UNSUPPORTED',m.file,'Unresolved Slot style'));if(css)child.setAttribute('style',css+';'+(child.getAttribute('style')||''));}
1877
+ return [...fragment.childNodes].map(n=>n.nodeType===1?n.outerHTML:n.textContent).join('');
1878
+ }
1879
+
1301
1880
  if (ctx[tag]?.iamChildren !== undefined) return ctx[tag].iamChildren;
1881
+ if (ctx[tag]?.iamRenderChildren) return ctx[tag].iamRenderChildren(ctx);
1302
1882
  const routerPrimitive = externalImport(m, tag);
1303
- if (["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && routerPrimitive.name === "Outlet") return ctx.__iamOutlet?.iamChildren ?? "";
1883
+ if (['react-router','react-router-dom'].includes(routerPrimitive?.specifier)&&routerPrimitive.name==='Navigate')return '';
1884
+ if (["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && routerPrimitive.name === "Outlet") return ctx.__iamOutlet?.iamRenderChildren ? ctx.__iamOutlet.iamRenderChildren(ctx) : ctx.__iamOutlet?.iamChildren ?? "";
1304
1885
  if (
1305
- /^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
1886
+ !localTarget && /^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
1306
1887
  tag,
1307
1888
  )
1308
1889
  )
@@ -1310,12 +1891,26 @@ export async function analyze({
1310
1891
  const intrinsicAlias = typeof ctx[tag] === "string" && allowedTags.has(ctx[tag]);
1311
1892
  const importedPrimitive=m.imports.get(tag),knownExternalPrimitive=importedPrimitive?.specifier==='react-basics'&&['Text','Button','SubmitButton'].includes(importedPrimitive.name);
1312
1893
  let htmlTag = intrinsicAlias ? ctx[tag] : tag;
1894
+ if(provider)return children;
1313
1895
  if (tag === "html" || tag === "body") {
1314
1896
  htmlTag = "div";
1315
1897
  props["data-iam-adapter"] = tag;
1316
1898
  }
1317
1899
  if (/^[A-Z]/.test(tag) && !intrinsicAlias) {
1318
- if (tag === "Link" || ["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && ["Link", "NavLink"].includes(routerPrimitive.name)) {
1900
+ if(tabPart){
1901
+ htmlTag=tabPart==='Tab'?'button':'div';
1902
+ if(tabPart==='List')props.role='tablist';
1903
+ if(tabPart==='Tab'){props.role='tab';props.type='button';props['aria-selected']=tabGroup?.initialValue===props.value;}
1904
+ if(tabPart==='Panel'){props.role='tabpanel';props.hidden=tabGroup?.initialValue!==props.value;}
1905
+ }
1906
+ else if(overlay){
1907
+ htmlTag=['Trigger','Close','Item','CheckboxItem','RadioItem','SubmenuTrigger'].includes(overlay.part)&&props.asChild!==true?'button':overlay.part==='Title'?'h2':overlay.part==='Description'?'p':'div';
1908
+ if(htmlTag==='button')props.type='button';
1909
+ if(['Portal','Popup','Backdrop'].includes(overlay.part)||overlay.family==='Transition'){props.hidden=!overlayGroup?.open;if(overlayGroup?.condition){props['data-iam-condition']=overlayGroup.condition.conditionKey;props['data-iam-alternative']='true';props['data-iam-condition-default']='false';props['data-iam-unobserved']='true';}if(overlayGroup?.variable)uiBindings.push({property:'visible',value:overlayGroup.variable,source:sourceLocation(m,node,owner.symbol)});}
1910
+ if(['Root','SubmenuRoot','Portal','Provider'].includes(overlay.part)||props.asChild===true)props.style={display:'contents'};
1911
+ if(overlay.part==='Popup')props.role=overlay.family==='Menu'?'menu':overlay.family==='Tooltip'?'tooltip':'dialog';
1912
+ }
1913
+ else if (tag === "Link" || ["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && ["Link", "NavLink"].includes(routerPrimitive.name)) {
1319
1914
  htmlTag = "a";
1320
1915
  if (typeof props.to === "string") props.href = props.to;
1321
1916
  }
@@ -1335,13 +1930,17 @@ export async function analyze({
1335
1930
  .join(" ");
1336
1931
  } else {
1337
1932
  const ref = componentBinding(m, tag),
1338
- target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
1933
+ target = ctx[tag]?.[COMPONENT]??(ref && componentDefs.get(ref.module.file + "#" + ref.name));
1339
1934
  if (target) {
1340
1935
  const own = components.get(owner.key);
1341
1936
  if (!own.dependencies.includes(target.key))
1342
1937
  own.dependencies.push(target.key);
1343
- const childProps = { ...props, children: { iamChildren: children }, __iamOutlet: ctx.__iamOutlet };
1344
- forwarded.set(childProps, { bindings: uiBindings, handlers: [...uiHandlers,...forwardedHandlers] });
1938
+ const hasChildren=ts.isJsxElement(node)&&node.children.some(n=>!ts.isJsxText(n)||jsxText(n.text).trim());
1939
+ const meaningful=ts.isJsxElement(node)?node.children.filter(n=>!ts.isJsxText(n)||jsxText(n.text).trim()):[];
1940
+ const childCallback=meaningful.length===1&&ts.isJsxExpression(meaningful[0])?unwrap(meaningful[0].expression):null;
1941
+ const callbackBody=childCallback&&(ts.isArrowFunction(childCallback)||ts.isFunctionExpression(childCallback))&&!childCallback.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&(!ts.isBlock(childCallback.body)||childCallback.body.statements.every(s=>ts.isVariableStatement(s)||ts.isIfStatement(s)||ts.isReturnStatement(s)))&&hasSourceJsx(uiReturnExpression(childCallback));
1942
+ const childProps = { ...props, ...(hasChildren?{children:callbackBody?{iamRenderProp:{fn:childCallback,owner,ctx,instance:instance+'/render-child'}}:{iamRenderChildren:renderChildren}}:{}), __iamCollectionTemplate:ctx.__iamCollectionTemplate, __iamOutlet: ctx.__iamOutlet, __iamContexts:ctx.__iamContexts, __iamTabs:ctx.__iamTabs, __iamOverlays:ctx.__iamOverlays };
1943
+ forwarded.set(childProps, { bindings: uiBindings, handlers: [...uiHandlers,...forwardedHandlers], attributes:uiAttributes });
1345
1944
  return renderComponent(
1346
1945
  target,
1347
1946
  childProps,
@@ -1349,6 +1948,23 @@ export async function analyze({
1349
1948
  depth + 1,
1350
1949
  );
1351
1950
  }
1951
+ if(registry){
1952
+ const regionKey=identity('ui-region',{page:pageKey,instance,tag}),regionNode=identity('node',{page:pageKey,instance,regionKey});
1953
+ const source=sourceLocation(m,opening,owner.symbol),elementKey=identity('element',{component:owner.key,regionOffset:opening.getStart(m.source)-owner.fn.getStart(m.source)});
1954
+ elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(regionNode,{elementKey,source,synthetic:true});
1955
+ const row={regionKey,routeKey:pageKey,renderKey:'pending',nodeId:regionNode,expression:redactLiteral(dynamicNode.getText(m.source)).slice(0,160),variableKey:registry.variableKey,initialValue:ui.variables.find(v=>v.variableKey===registry.variableKey)?.initialValue??null,options:[],source};
1956
+ if(typeof row.initialValue!=='string'||!registry.options.some(o=>o.value===row.initialValue))row.initialValue=null;
1957
+ let html='<div data-iam-node="'+regionNode+'" data-iam-region="'+regionKey+'">';
1958
+ for(const option of registry.options){
1959
+ const id=identity('node',{page:pageKey,regionKey,value:option.value}),optionElement=identity('element',{component:owner.key,regionKey,value:option.value});
1960
+ elements.set(optionElement,{elementKey:optionElement,componentKey:owner.key,tag:'div',source:option.source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(id,{elementKey:optionElement,source:option.source,synthetic:true});
1961
+ const catalog=ctx.t?.[TRANSLATION]??(translationResources.size===1?[...translationResources][0]:null),title=catalog?option.label.split('.').reduce((v,key)=>v?.[key],catalog):undefined;
1962
+ row.options.push({value:option.value,label:typeof title==='string'?title:option.label,nodeId:id,componentKey:option.component.key,source:option.source,scope:option.scope});
1963
+ const own=components.get(owner.key);if(!own.dependencies.includes(option.component.key))own.dependencies.push(option.component.key);
1964
+ html+='<div data-iam-node="'+id+'" data-iam-region-value="'+escape(option.value)+'" data-iam-unobserved="true" hidden="">'+renderComponent(option.component,{...props,children:{iamRenderChildren:renderChildren},__iamOutlet:ctx.__iamOutlet,__iamContexts:ctx.__iamContexts,__iamOverlays:ctx.__iamOverlays},instance+'/registry-'+option.value,depth+1)+'</div>';
1965
+ }
1966
+ html+='</div>';presentation.regions.push(row);gap('DYNAMIC_COMPONENT_CANDIDATES',m.file,tag+': finite registry alternatives retained as source-only UI previews; selection and scope are unobserved');return html;
1967
+ }
1352
1968
  htmlTag = /Button|Trigger/.test(tag)
1353
1969
  ? "button"
1354
1970
  : /Input/.test(tag)
@@ -1361,6 +1977,10 @@ export async function analyze({
1361
1977
  }
1362
1978
  }
1363
1979
  if (!allowedTags.has(htmlTag)) return children;
1980
+ const reveal=ctx.__iamOverlays?.get('Transition')?.reveal,ref=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text==='ref');
1981
+ if(reveal?.file===m.file&&reveal.symbol===owner.symbol&&ref?.initializer&&ts.isJsxExpression(ref.initializer)&&ts.isIdentifier(ref.initializer.expression)&&ref.initializer.expression.text===reveal.ref&&props.style?.display==='none'){
1982
+ const {display,...style}=props.style;props.style=style;
1983
+ }
1364
1984
  const anchor = anchorAttributes
1365
1985
  .map((key) =>
1366
1986
  typeof props[key] === "string" ? key + "=" + props[key] : null,
@@ -1385,6 +2005,7 @@ export async function analyze({
1385
2005
  element: elementKey,
1386
2006
  instance,
1387
2007
  });
2008
+ if(!ctx.__iamCollectionTemplate)for(const request of uiAttributes)attributeRequests.push({nodeId,...request});
1388
2009
  elements.set(elementKey, {
1389
2010
  elementKey,
1390
2011
  componentKey: owner.key,
@@ -1401,13 +2022,34 @@ export async function analyze({
1401
2022
  source,
1402
2023
  synthetic: htmlTag !== tag && tag !== "Link" && tag !== "Image"&&!intrinsicAlias&&!knownExternalPrimitive,
1403
2024
  });
1404
- for (const handler of uiHandlers) {
2025
+ if(tabPart==='Root'){
2026
+ // Tab initialization below shares the same concrete source container.
2027
+ tabGroup.nodeId=nodeId;
2028
+ const {controller,...row}=tabGroup;
2029
+ if(row.items.length&&row.initialValue!==null&&!row.items.some(item=>item.value===row.initialValue))row.initialValue=null;
2030
+ presentation.tabs.push(row);
2031
+ }
2032
+ if(overlayGroup?.controller&&overlayGroup.variable&&['Dialog','AlertDialog','Menu','Popover','Collapsible'].includes(overlay?.family)&&['Trigger','Close'].includes(overlay.part)&&!uiHandlers.some(h=>h.key==='onClick')){
2033
+ const next=overlay.part==='Close'?{op:'const',value:false}:{op:'not',arg:overlayGroup.variable};
2034
+ const replace=expression=>expression.op==='event'&&expression.key==='value'?next:Object.fromEntries(Object.entries(expression).map(([key,value])=>[key,Array.isArray(value)?value.map(v=>v&&typeof v==='object'?replace(v):v):value&&typeof value==='object'?replace(value):value]));
2035
+ const controller=overlayGroup.controller,writes=(controller.writes||[{variableKey:controller.variableKey,value:controller.value}]).map(w=>({...w,value:replace(w.value)}));
2036
+ uiHandlers.push({...writes[0],writes,key:'onClick',payload:{},source});
2037
+ }
2038
+ if(tabPart==='Tab'&&tabGroup&&typeof props.value==='string'&&props.value.length<=2048){
2039
+ if(!tabGroup.items.some(item=>item.value===props.value))tabGroup.items.push({value:props.value,nodeId});
2040
+ props['data-iam-tab-value']=props.value;props['data-iam-tab-key']=tabGroup.tabKey;
2041
+ const replace=value=>value.op==='event'&&value.key==='value'?{op:'const',value:props.value}:Object.fromEntries(Object.entries(value).map(([k,v])=>[k,Array.isArray(v)?v.map(item=>item&&typeof item==='object'?replace(item):item):v&&typeof v==='object'?replace(v):v]));
2042
+ if(tabGroup.controller){const writes=(tabGroup.controller.writes||[{variableKey:tabGroup.controller.variableKey,value:tabGroup.controller.value}]).map(w=>({...w,value:replace(w.value)}));uiHandlers.push({...writes[0],writes,key:'onClick',payload:{},source});}
2043
+ }
2044
+ if(tabPart==='Panel'&&tabGroup?.variableKey&&typeof props.value==='string')uiBindings.push({property:'visible',value:{op:'eq',left:{op:'var',key:tabGroup.variableKey},right:{op:'const',value:props.value}},source});
2045
+ for (const handler of uiHandlers.filter(h=>!['onValueChange','onOpenChange'].includes(h.key))) {
1405
2046
  const event = handler.key === "onClick" ? "click" : htmlTag === "select" || handler.payload.checked ? "change" : "input";
1406
- ui.transitions.push({ transitionKey: identity("ui-transition", { page: pageKey, node: nodeId, event }), routeKey: pageKey, fromStateKey: "pending", toStateKey: "pending", trigger: { nodeId, event, payload: handler.payload }, guard: { op: "const", value: true }, stateReads: readVariables(handler.value), stateWrites: [{ variableKey: handler.variableKey, value: handler.value }], simulation: "implemented", evidence: "source_inferred", source: handler.source });
2047
+ const writes=handler.writes||[{variableKey:handler.variableKey,value:handler.value}];
2048
+ ui.transitions.push({ transitionKey: identity("ui-transition", { page: pageKey, node: nodeId, event }), routeKey: pageKey, fromStateKey: "pending", toStateKey: "pending", trigger: { nodeId, event, payload: handler.payload }, guard: { op: "const", value: true }, stateReads: [...new Set(writes.flatMap(w=>readVariables(w.value)))].sort(), stateWrites: writes, simulation: "implemented", evidence: "source_inferred", source: handler.source });
1407
2049
  }
1408
- for (const binding of uiBindings.filter(b => ["value", "checked", "disabled", "className"].includes(b.property))) {
2050
+ for (const binding of uiBindings.filter(b => !ctx.__iamCollectionTemplate&&["value", "checked", "disabled", "className", "visible"].includes(b.property)&&!(tabPart==='Root'&&b.property==='value'))) {
1409
2051
  if(['value','checked'].includes(binding.property)&&!['input','select','textarea'].includes(htmlTag)){gap('UI_BINDING_TARGET_UNSUPPORTED',m.file,tag+': input property cannot be simulated on a non-input adapter');continue;}
1410
- const expectedType = ["checked", "disabled"].includes(binding.property) ? "boolean" : "string";
2052
+ const expectedType = ["checked", "disabled", "visible"].includes(binding.property) ? "boolean" : "string";
1411
2053
  if (uiExpressionType(binding.value) !== expectedType) {
1412
2054
  gap("UI_BINDING_TYPE_UNSUPPORTED", m.file, tag + "." + binding.property + ": source value cannot be represented by the typed UI simulation");
1413
2055
  continue;
@@ -1421,7 +2063,7 @@ export async function analyze({
1421
2063
  ui.transitions = ui.transitions.filter(t => t.trigger.nodeId !== nodeId);
1422
2064
  gap("SENSITIVE_INPUT_REDACTED", m.file, "Input value and state bridge omitted");
1423
2065
  }
1424
- const attrs = { "data-iam-node": nodeId };
2066
+ const attrs = { "data-iam-node": nodeId,...(['input','textarea','select'].includes(htmlTag)&&Object.hasOwn(props,'value')&&isUnknown(props.value)?{'data-iam-input-unobserved':'true'}:{}) };
1425
2067
  for (const [key, value] of Object.entries(props)) {
1426
2068
  if (key === "style") {
1427
2069
  const css = staticStyle(value, () => gap("INLINE_STYLE_UNSUPPORTED", m.file,
@@ -1449,7 +2091,7 @@ export async function analyze({
1449
2091
  clipRule: "clip-rule",
1450
2092
  }[key] || key;
1451
2093
  if (
1452
- !/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|width|height|viewBox|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule|cx|cy|r|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-indiana-id|data-indiana-map-(?:area|flow|kind|auth|edge)|data-testid|data-test|data-qa|data-iam-(?:adapter|toggle))$/.test(
2094
+ !/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|width|height|viewBox|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule|cx|cy|r|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-indiana-id|data-indiana-map-(?:area|flow|kind|auth|edge)|data-(?:slot|state|side|variant|collapsible|orientation|active)|data-testid|data-test|data-qa|data-iam-(?:adapter|toggle|tab-value|tab-key|condition|alternative|condition-default|unobserved))$/.test(
1453
2095
  attr,
1454
2096
  )
1455
2097
  )
@@ -1503,22 +2145,38 @@ export async function analyze({
1503
2145
  }
1504
2146
  return renderComponent(entryDef, props, instance, 0);
1505
2147
  }
1506
- function renderBranch(branch) {
1507
- let content = renderEntry(branch.file, {}, branch === composition.composition ? "page" : "slot-page:" + branch.file, branch.symbol);
1508
- for (const layer of branch.layers) {
2148
+ function renderBranch(branch, inherited = {}) {
2149
+ const contentId=identity('node',{page:pageKey,lane:branch.lane,pageContent:true,...(composition.entryMode==='soft'?{composition:composition.compositionKey}:{})}),contentElement=identity('element',{page:pageKey,lane:branch.lane,pageContent:true});
2150
+ const contentSource=sourceLocation(modules.get(branch.file),modules.get(branch.file).source,branch.symbol??'$page-content');
2151
+ elements.set(contentElement,{elementKey:contentElement,componentKey:def.key,tag:'div',source:contentSource,identityStrategy:'page_root',editableProperties:[],binding:'preview_only'});record.set(contentId,{elementKey:contentElement,source:contentSource,synthetic:true});
2152
+ const contextProps=ctx=>({__iamContexts:ctx.__iamContexts,__iamTabs:ctx.__iamTabs,__iamOverlays:ctx.__iamOverlays});
2153
+ function renderLayer(index,ctx){
2154
+ if(index<0){
2155
+ const content=renderEntry(branch.file,contextProps(ctx),branch===composition.composition?'page':'slot-page:'+branch.file,branch.symbol);
2156
+ return '<div data-iam-node="'+contentId+'" data-iam-page-content="true">'+content+'</div>';
2157
+ }
2158
+ const layer=branch.layers[index],child={iamRenderChildren:next=>renderLayer(index-1,next)};
1509
2159
  const props = ["react-router", "tanstack-router"].includes(routing.framework)
1510
- ? { __iamOutlet: { iamChildren: content } }
1511
- : { children: { iamChildren: content } };
1512
- if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component = { iamChildren: content }; props.pageProps = {}; }
2160
+ ? { ...contextProps(ctx), __iamOutlet: child }
2161
+ : { ...contextProps(ctx), children: child };
2162
+ if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component = child; props.pageProps = {}; }
1513
2163
  for (const slot of layer.slots) {
1514
- props[slot.name] = { iamChildren: renderBranch(slot.branch) };
2164
+ props[slot.name] = { iamRenderChildren:next=>renderBranch(slot.branch,next) };
1515
2165
  if (composition.entryMode === "hard") gap("SOFT_NAVIGATION_STATE_UNOBSERVED", layer.file, "Slot " + slot.name + ": hard-load composition alone does not establish retained soft-navigation state");
1516
2166
  }
1517
- content = renderEntry(layer.file, props, "layout:" + path.posix.dirname(layer.file), layer.symbol);
2167
+ return renderEntry(layer.file, props, "layout:" + path.posix.dirname(layer.file), layer.symbol);
1518
2168
  }
1519
- return content;
2169
+ return renderLayer(branch.layers.length-1,inherited);
1520
2170
  }
1521
2171
  let html = composition.gaps.length ? "" : renderBranch(composition.composition);
2172
+ for(const request of attributeRequests){
2173
+ const tabs=presentation.tabs.filter(t=>request.reads.includes(t.variableKey));if(tabs.length!==1||tabs[0].items.length>8)continue;
2174
+ const tab=tabs[0],cases=[];
2175
+ for(const item of tab.items){const next={...request.ctx};for(const [label,key]of Object.entries(next.__iamVariableKeys))if(key===tab.variableKey)next[label]=item.value;for(const [label,expr]of next.__iamPropBindings)if(expr.op==='var'&&expr.key===tab.variableKey)next[label]=item.value;for(const [label,init]of next.__iamLocals)if(!next.__iamStates.has(label))next[label]=evalExpression(init,request.owner.module,next);const value=evalExpression(request.expression,request.owner.module,next);if(typeof value!=='string'||value.length>4000){cases.length=0;break;}cases.push({value:item.value,text:value});if(request.property==='class')value.split(/\s+/).filter(Boolean).forEach(c=>allClasses.add(c));}
2176
+ if(cases.length<2||new Set(cases.map(c=>c.text)).size<2)continue;
2177
+ const set={property:request.property,cases,source:request.source},setKey=identity('ui-attribute-set',set);if(!presentation.attributeSets.some(s=>s.setKey===setKey))presentation.attributeSets.push({setKey,...set});
2178
+ const row={routeKey:pageKey,renderKey:'pending',nodeId:request.nodeId,variableKey:tab.variableKey,setKey};presentation.attributes.push({attributeKey:identity('ui-attribute',row),...row});
2179
+ }
1522
2180
  const rootId = identity("node", { page: pageKey, root: true, ...(composition.entryMode === "soft" ? { composition: composition.compositionKey } : {}) }),
1523
2181
  rootElement = identity("element", { page: pageKey, root: true });
1524
2182
  record.set(rootId, {
@@ -1636,21 +2294,12 @@ export async function analyze({
1636
2294
  source: page.source,
1637
2295
  documentKey,
1638
2296
  });
1639
- for (const branch of branches.slice(0, 30)) {
1640
- const key = identity("state", {
1641
- page: pageKey,
1642
- source: branch.source,
1643
- label: branch.label,
1644
- });
1645
- if (!states.some((s) => s.stateKey === key)) {
1646
- states.push({
1647
- stateKey: key,
1648
- pageKey,
1649
- label: branch.label,
1650
- evidence: branch.evidence,
1651
- source: branch.source,
1652
- documentKey: null,
1653
- });
2297
+ // Unknown source alternatives live in ui.conditions. Representable local
2298
+ // state candidates remain separate from concrete source documents.
2299
+ for (const branch of branches.filter(b=>b.evidence==='source_inferred').slice(0, 30)) {
2300
+ const key = identity("state", {page:pageKey,source:branch.source,label:branch.label});
2301
+ if(!states.some(state=>state.stateKey===key)){
2302
+ states.push({stateKey:key,pageKey,label:branch.label,evidence:branch.evidence,source:branch.source,documentKey:null});
1654
2303
  page.stateKeys.push(key);
1655
2304
  }
1656
2305
  }
@@ -1679,7 +2328,7 @@ export async function analyze({
1679
2328
  for (const node of doc.nodes) {
1680
2329
  if (
1681
2330
  node.kind !== "element" ||
1682
- !["a", "button", "form"].includes(node.tag)
2331
+ node.tag !== "a"
1683
2332
  )
1684
2333
  continue;
1685
2334
  const href = node.attributes.href || "",
@@ -1713,9 +2362,18 @@ export async function analyze({
1713
2362
  });
1714
2363
  }
1715
2364
  const packagedAssets = [];
2365
+ inventory.push(...await referencedRasterSources(root,assets.keys()));
1716
2366
  for (const [url] of assets) {
1717
2367
  gap("PUBLIC_ASSET_NOT_PACKAGED", "public", url + ": requires an explicit public-asset build input");
1718
2368
  }
2369
+ // Only statically imported styles belong to the source entry's default presentation.
2370
+ const styleFiles=new Map(cssSources.map(s=>[s.file,s])),activeStyles=new Map();
2371
+ const visitStyle=file=>{const style=styleFiles.get(file);if(!style||activeStyles.has(file))return;activeStyles.set(file,style);for(const match of style.text.matchAll(/@import\s+["']([^"']+)["']/g))if(match[1].startsWith('.'))visitStyle(path.posix.normalize(path.posix.join(path.posix.dirname(file),match[1])));};
2372
+ for(const module of selection.discoveryModules.values())for(const statement of module.source.statements)if(ts.isImportDeclaration(statement)&&statement.moduleSpecifier&&ts.isStringLiteral(statement.moduleSpecifier)){
2373
+ const spec=statement.moduleSpecifier.text;if(spec.endsWith('.css')&&spec.startsWith('.'))visitStyle(path.posix.normalize(path.posix.join(path.posix.dirname(module.file),spec)));
2374
+ }
2375
+ const sourceStyles=[...activeStyles.values()],sourceThemes=sourceStyles.flatMap(s=>[...s.text.matchAll(/@theme(?:\s+inline)?\s*\{([^{}]*)\}/g)].map(m=>m[0])).join('\n');
2376
+ const color=t=>sourceStyles.some(s=>new RegExp('--'+t+':\\s*(?:oklch|oklab|rgb|hsl|lab|lch|color|var)\\(|--'+t+':\\s*#').test(s.text))?'var(--'+t+')':'hsl(var(--'+t+'))';
1719
2377
  let css = "";
1720
2378
  try {
1721
2379
  const tw = dependency("tailwindcss");
@@ -1746,16 +2404,16 @@ export async function analyze({
1746
2404
  ];
1747
2405
  const custom =
1748
2406
  "@theme inline {" +
1749
- tokens.map((t) => "--color-" + t + ":hsl(var(--" + t + "));").join("") +
2407
+ tokens.map((t) => "--color-" + t + ":" + color(t) + ";").join("") +
1750
2408
  "--font-heading: ui-sans-serif,system-ui;--radius-lg:var(--radius);--radius-md:calc(var(--radius) - 2px);}";
1751
2409
  const compile = await tw.value.compile(
1752
- theme + "\n" + custom + "\n@tailwind utilities;",
2410
+ theme + "\n" + custom + "\n" + sourceThemes + "\n@tailwind utilities;",
1753
2411
  );
1754
2412
  css = compile.build([...allClasses].sort());
1755
2413
  } catch (error) {
1756
2414
  gap("STYLE_COMPILER", "", error.message);
1757
2415
  }
1758
- const vars = cssSources
2416
+ const vars = sourceStyles
1759
2417
  .flatMap((s) =>
1760
2418
  [...s.text.matchAll(/(?::root|\.dark)\s*\{([^{}]*)\}/g)].map(
1761
2419
  (m) => m[0],
@@ -1765,7 +2423,7 @@ export async function analyze({
1765
2423
  css +=
1766
2424
  "\n" +
1767
2425
  vars +
1768
- "\n*{box-sizing:border-box;border-color:hsl(var(--border));border-style:solid;border-width:0}body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;color:hsl(var(--foreground));background:hsl(var(--background))}h1,h2,h3,p,ul,ol{margin:0}a{color:inherit;text-decoration:none}button,input,textarea,select{font:inherit}button,a{cursor:pointer}button{background:transparent}svg{display:block}img{max-width:100%;height:auto}.container{width:100%;max-width:1400px;padding-inline:2rem;margin-inline:auto}.iam-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1em;height:1.1em}.iam-page{min-height:100vh}.iam-placeholder{padding:1rem;border:1px dashed #d4d4d4}";
2426
+ "\n*{box-sizing:border-box;border-color:"+color("border")+";border-style:solid;border-width:0}[hidden]{display:none!important}body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;color:"+color("foreground")+";background:"+color("background")+"}h1,h2,h3,p,ul,ol{margin:0}a{color:inherit;text-decoration:none}button,input,textarea,select{font:inherit}button,a{cursor:pointer}button{background:transparent}svg{display:block}img{max-width:100%;height:auto}.container{width:100%;max-width:1400px;padding-inline:2rem;margin-inline:auto}.iam-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1em;height:1.1em}.iam-page{min-height:100vh}.iam-placeholder{padding:1rem;border:1px dashed #d4d4d4}";
1769
2427
  const runtime = await fs.readFile(
1770
2428
  new URL("../assets/document-runtime.js", import.meta.url),
1771
2429
  "utf8",
@@ -1781,6 +2439,16 @@ export async function analyze({
1781
2439
  doc.js = runtime;
1782
2440
  sealDocument(doc);
1783
2441
  }
2442
+ // Inventory every source file, but serialize UI definitions reachable from
2443
+ // projected routes (including retained alternatives and shared fragments).
2444
+ const liveElements=new Set([...documents.flatMap(d=>d.nodes.map(n=>n.elementKey)),...presentation.fragments.flatMap(f=>f.sourceElements)]);
2445
+ for(const key of elements.keys())if(!liveElements.has(key))elements.delete(key);
2446
+ const liveComponents=new Set([...pages.flatMap(p=>p.componentKeys),...[...elements.values()].map(e=>e.componentKey)]);
2447
+ const compositionFiles=new Set(renderDefinitions.flatMap(d=>flattenComposition(d.composition).flatMap(entry=>[entry.file,...entry.layers.map(layer=>layer.file)])));
2448
+ for(const component of components.values())if(compositionFiles.has(component.file))liveComponents.add(component.componentKey);
2449
+ const retainComponent=key=>{const c=components.get(key);if(!c)return;for(const dep of c.dependencies)if(!liveComponents.has(dep)){liveComponents.add(dep);retainComponent(dep);}};
2450
+ for(const key of [...liveComponents])retainComponent(key);
2451
+ for(const key of components.keys())if(!liveComponents.has(key))components.delete(key);
1784
2452
  const sourceGraphDigest = hashObject("iam/source-graph/v1", {
1785
2453
  files: inventory,
1786
2454
  components: [...components.values()],
@@ -1790,8 +2458,8 @@ export async function analyze({
1790
2458
  schemaVersion: 2,
1791
2459
  producer: {
1792
2460
  name: "indiana-app-manifest-local",
1793
- version: "0.19.0",
1794
- method: "typescript-ast-and-bounded-static-projection",
2461
+ version: "0.24.0",
2462
+ method: "typescript-ast-and-bounded-static-projection;lucide-static-"+LUCIDE_SOURCE_VERSION,
1795
2463
  },
1796
2464
  app: { name, platforms: ["web"] },
1797
2465
  routing,
@@ -1824,7 +2492,29 @@ export async function analyze({
1824
2492
  ui.bindings = ui.bindings.filter(b => concrete.has(b.nodeId));
1825
2493
  ui.transitions = ui.transitions.filter(t => concrete.has(t.trigger.nodeId));
1826
2494
  for (const doc of documents) for (const node of doc.nodes) if (node.kind === "element" && ["button", "form", "input", "select", "textarea"].includes(node.tag) && !ui.transitions.some(t => t.trigger.nodeId === node.nodeId)) ui.uncovered.push({ routeKey: doc.pageKey, nodeId: node.nodeId, event: "source-event", reason: "No supported UI-only handler; business callbacks are not executed", source: node.source });
2495
+ // State with no representable UI read/write has no preview effect.
2496
+ const usedVariables=new Set([...ui.bindings.flatMap(b=>readVariables(b.value)),...ui.transitions.flatMap(t=>[...t.stateReads,...t.stateWrites.map(w=>w.variableKey)]),...[...presentation.tabs,...presentation.regions,...presentation.collections].map(t=>t.variableKey).filter(Boolean),...presentation.collections.flatMap(c=>c.requires.map(r=>r.variableKey))]);
2497
+ ui.variables=ui.variables.filter(v=>usedVariables.has(v.variableKey));
2498
+ // Repeated instances of the same unsupported source control share one source gap.
2499
+ const sourceGaps=new Map();
2500
+ for(const row of ui.uncovered){const key=JSON.stringify([row.routeKey,row.source,row.event,row.reason]);const prior=sourceGaps.get(key);if(prior)prior.nodeId=null;else sourceGaps.set(key,{...row});}
2501
+ ui.uncovered=[...sourceGaps.values()];
1827
2502
  manifest.routeTree = assembleRouteTree(manifest, ui, renderDefinitions, compositions.navigation);
2503
+ presentation.conditions=[...new Map(presentation.conditions.map(c=>[c.conditionKey,c])).values()];
2504
+ presentation.regions=presentation.regions.filter(r=>concrete.has(r.nodeId)&&r.options.every(o=>concrete.has(o.nodeId)));
2505
+ presentation.tabs=presentation.tabs.filter(t=>concrete.has(t.nodeId)&&t.items.length&&t.items.every(i=>concrete.has(i.nodeId)));
2506
+ presentation.attributes=presentation.attributes.filter(a=>concrete.has(a.nodeId));presentation.attributeSets=presentation.attributeSets.filter(s=>presentation.attributes.some(a=>a.setKey===s.setKey));
2507
+ presentation.navigation=sourceRedirects({ts,modules,binding,functionFrom,externalImport,sourceLocation,constant:(node,module)=>{const result=evalExpression(node,module,{});return isUnknown(result)?undefined:result;},pages,components:[...components.values()],documents});
2508
+ manifest.routeTree=attachUiStructure(manifest.routeTree,presentation,documents);
2509
+ // Discarded adapter children have no concrete UI. Keep every document and
2510
+ // shared-fragment mapping, but do not serialize unreachable element records.
2511
+ const referencedElements=new Set([...documents.flatMap(d=>d.nodes.map(n=>n.elementKey)),...presentation.fragments.flatMap(f=>f.sourceElements)]);
2512
+ manifest.elements=manifest.elements.filter(element=>referencedElements.has(element.elementKey));
2513
+ // Each navigation row references its exact existing interaction. The original
2514
+ // interaction ID remains in the authoritative array, instead of being repeated.
2515
+ const interactionIndices=new Map(manifest.interactions.map((row,index)=>[row.interactionKey,index]));
2516
+ for(const row of manifest.routeTree.navigation)row.interactionKey=interactionIndices.get(row.interactionKey);
2517
+ internGraphSources(manifest.routeTree);
1828
2518
  manifest.integrity.routeTreeDigest = routeIndexDigest(manifest.routeTree);
1829
2519
  const concreteNodes = new Map(documents.flatMap(doc => doc.nodes.map(node => [node.nodeId, node])));
1830
2520
  for (const target of manifest.routeTree.targets) {