react-pebble 0.1.1 → 0.2.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.
@@ -78,6 +78,7 @@ function colorToHex(name: string): string {
78
78
  }
79
79
 
80
80
  const FONT_TO_PIU: Record<string, string> = {
81
+ // Gothic family
81
82
  gothic14: '14px Gothic',
82
83
  gothic14Bold: 'bold 14px Gothic',
83
84
  gothic18: '18px Gothic',
@@ -86,9 +87,25 @@ const FONT_TO_PIU: Record<string, string> = {
86
87
  gothic24Bold: 'bold 24px Gothic',
87
88
  gothic28: '28px Gothic',
88
89
  gothic28Bold: 'bold 28px Gothic',
90
+ // Bitham family
89
91
  bitham30Black: 'black 30px Bitham',
90
92
  bitham42Bold: 'bold 42px Bitham',
91
93
  bitham42Light: 'light 42px Bitham',
94
+ bitham34MediumNumbers: '34px Bitham',
95
+ bitham42MediumNumbers: '42px Bitham',
96
+ // Roboto family
97
+ robotoCondensed21: '21px Roboto Condensed',
98
+ roboto21: '21px Roboto',
99
+ // Droid Serif
100
+ droid28: '28px Droid Serif',
101
+ // LECO family
102
+ leco20: '20px LECO',
103
+ leco26: '26px LECO',
104
+ leco28: '28px LECO',
105
+ leco32: '32px LECO',
106
+ leco36: '36px LECO',
107
+ leco38: '38px LECO',
108
+ leco42: '42px LECO',
92
109
  };
93
110
 
94
111
  function fontToPiu(name: string | undefined): string {
@@ -323,6 +340,24 @@ function detectUseMessage(exName: string): MessageInfo | null {
323
340
  const messageInfo = detectUseMessage(entryPath);
324
341
  if (messageInfo) {
325
342
  process.stderr.write(`useMessage detected: key="${messageInfo.key}"${messageInfo.mockDataArrayName ? ` mockData=${messageInfo.mockDataArrayName}` : ''}\n`);
343
+
344
+ // Extract the mock data value from the source so the plugin can generate phone-side JS
345
+ if (messageInfo.mockDataArrayName) {
346
+ const sf = parseExampleSource(entryPath);
347
+ if (sf) {
348
+ walkAST(sf, (node) => {
349
+ if (
350
+ ts.isVariableDeclaration(node) &&
351
+ ts.isIdentifier(node.name) &&
352
+ node.name.text === messageInfo.mockDataArrayName &&
353
+ node.initializer
354
+ ) {
355
+ const mockDataSource = node.initializer.getText(sf);
356
+ process.stderr.write(`mockDataValue=${mockDataSource}\n`);
357
+ }
358
+ });
359
+ }
360
+ }
326
361
  }
327
362
 
328
363
  /** Module-level map collecting string enum values per slot from handler analysis */
@@ -407,6 +442,32 @@ function analyzeSetterCall(call: ts.CallExpression, sf: ts.SourceFile): HandlerA
407
442
  if (ts.isPrefixUnaryExpression(body) && body.operator === ts.SyntaxKind.ExclamationToken) {
408
443
  return { type: 'toggle', slotIndex, value: 0 };
409
444
  }
445
+ // param => Math.min(param + N, max) (clamped increment)
446
+ // param => Math.max(param - N, min) (clamped decrement)
447
+ if (ts.isCallExpression(body) && ts.isPropertyAccessExpression(body.expression)) {
448
+ const obj = body.expression.expression;
449
+ const method = body.expression.name.text;
450
+ if (ts.isIdentifier(obj) && obj.text === 'Math' && body.arguments.length === 2) {
451
+ const [a0, a1] = [body.arguments[0]!, body.arguments[1]!];
452
+ if (method === 'min' && ts.isBinaryExpression(a0)) {
453
+ if (a0.operatorToken.kind === ts.SyntaxKind.PlusToken && ts.isNumericLiteral(a0.right)) {
454
+ return { type: 'increment', slotIndex, value: Number(a0.right.text) };
455
+ }
456
+ }
457
+ if (method === 'max' && ts.isBinaryExpression(a0)) {
458
+ if (a0.operatorToken.kind === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(a0.right)) {
459
+ return { type: 'decrement', slotIndex, value: Number(a0.right.text) };
460
+ }
461
+ }
462
+ }
463
+ }
464
+ // param => (param + N) % M (modular increment)
465
+ if (ts.isBinaryExpression(body) && body.operatorToken.kind === ts.SyntaxKind.PercentToken) {
466
+ const left = ts.isParenthesizedExpression(body.left) ? body.left.expression : body.left;
467
+ if (ts.isBinaryExpression(left) && left.operatorToken.kind === ts.SyntaxKind.PlusToken && ts.isNumericLiteral(left.right)) {
468
+ return { type: 'increment', slotIndex, value: Number(left.right.text) };
469
+ }
470
+ }
410
471
  }
411
472
 
412
473
  return null;
@@ -586,6 +647,15 @@ function detectListPatterns(exName: string): ListInfo | null {
586
647
  // Emit context
587
648
  // ---------------------------------------------------------------------------
588
649
 
650
+ interface ElementPos {
651
+ type: string;
652
+ left: number;
653
+ top: number;
654
+ width: number;
655
+ height: number;
656
+ radius?: number;
657
+ }
658
+
589
659
  interface EmitContext {
590
660
  skins: Map<string, string>;
591
661
  styles: Map<string, string>;
@@ -598,6 +668,10 @@ interface EmitContext {
598
668
  labelTexts: Map<number, string>;
599
669
  /** Map from rect sequential index → its fill color name */
600
670
  rectFills: Map<number, string>;
671
+ /** Sequential index for all positioned elements (circles, rects) */
672
+ elemIdx: number;
673
+ /** Map from element sequential index → its position at this render */
674
+ elementPositions: Map<number, ElementPos>;
601
675
  }
602
676
 
603
677
  function ensureSkin(ctx: EmitContext, fill: string): string {
@@ -632,6 +706,7 @@ interface ConditionalChild {
632
706
  }
633
707
  const conditionalChildren: ConditionalChild[] = [];
634
708
  let emitConditionals = false; // Only wrap conditionals during final emit
709
+ const animatedElemNames = new Set<number>(); // Element indices that need names for animation
635
710
  let conditionalDepth = 0; // Track nesting — only wrap at depth 1 (root Group's children)
636
711
 
637
712
  // Emit piu tree
@@ -714,10 +789,14 @@ function emitNode(
714
789
  // Track rect fill for skin reactivity detection
715
790
  const rectIdx = ctx.rectIdx++;
716
791
  ctx.rectFills.set(rectIdx, fill);
792
+ // Track rect position for animation keyframing
793
+ const rEIdx = ctx.elemIdx++;
794
+ ctx.elementPositions.set(rEIdx, { type: 'rect', left: x, top: y, width: w, height: h });
717
795
 
718
- // If this rect has a dynamic skin, give it a name
796
+ // If this rect has a dynamic skin or animated position, give it a name
719
797
  const isSkinDynamic = skinDeps?.has(rectIdx) ?? false;
720
- const nameProp = isSkinDynamic ? `, name: "sr${rectIdx}"` : '';
798
+ const isAnimated = animatedElemNames.has(rEIdx);
799
+ const nameProp = isSkinDynamic ? `, name: "sr${rectIdx}"` : isAnimated ? `, name: "ae${rEIdx}"` : '';
721
800
 
722
801
  // Use constraint-based layout when dimensions match screen size
723
802
  // (so the output adapts to any screen at runtime)
@@ -801,7 +880,10 @@ function emitNode(
801
880
  const skinVar = ensureSkin(ctx, fill);
802
881
  // piu RoundRect with radius = r draws a circle when width = height = 2*r
803
882
  const size = r * 2;
804
- return `${indent}new RoundRect(null, { left: ${cx}, top: ${cy}, width: ${size}, height: ${size}, radius: ${r}, skin: ${skinVar} })`;
883
+ const eIdx = ctx.elemIdx++;
884
+ ctx.elementPositions.set(eIdx, { type: 'circle', left: cx, top: cy, width: size, height: size, radius: r });
885
+ const animName = animatedElemNames.has(eIdx) ? `, name: "ae${eIdx}"` : '';
886
+ return `${indent}new RoundRect(null, { left: ${cx}, top: ${cy}, width: ${size}, height: ${size}, radius: ${r}, skin: ${skinVar}${animName} })`;
805
887
  }
806
888
 
807
889
  default:
@@ -849,7 +931,7 @@ function buildSizeProps(x: number, y: number, w: number, h: number): string {
849
931
  // Time format inference
850
932
  // ---------------------------------------------------------------------------
851
933
 
852
- type TimeFormat = 'HHMM' | 'SS' | 'DATE';
934
+ type TimeFormat = 'HHMM' | 'MMSS' | 'SS' | 'DATE';
853
935
 
854
936
  function inferTimeFormat(textAtT1: string, t1: Date): TimeFormat | null {
855
937
  const hh = pad2(t1.getHours());
@@ -862,6 +944,7 @@ function inferTimeFormat(textAtT1: string, t1: Date): TimeFormat | null {
862
944
  ];
863
945
 
864
946
  if (textAtT1 === `${hh}:${mm}`) return 'HHMM';
947
+ if (textAtT1 === `${mm}:${ss}`) return 'MMSS';
865
948
  if (textAtT1 === ss) return 'SS';
866
949
  if (
867
950
  textAtT1.includes(days[t1.getDay()]!) &&
@@ -876,6 +959,8 @@ function emitTimeExpr(fmt: TimeFormat): string {
876
959
  switch (fmt) {
877
960
  case 'HHMM':
878
961
  return 'pad(d.getHours()) + ":" + pad(d.getMinutes())';
962
+ case 'MMSS':
963
+ return 'pad(d.getMinutes()) + ":" + pad(d.getSeconds())';
879
964
  case 'SS':
880
965
  return 'pad(d.getSeconds())';
881
966
  case 'DATE':
@@ -940,6 +1025,8 @@ const ctx1: EmitContext = {
940
1025
  labelTexts: new Map(),
941
1026
  rectIdx: 0,
942
1027
  rectFills: new Map(),
1028
+ elemIdx: 0,
1029
+ elementPositions: new Map(),
943
1030
  };
944
1031
  emitNode(app1._root, ctx1, '', null, null);
945
1032
  const t1Texts = new Map(ctx1.labelTexts);
@@ -955,7 +1042,7 @@ for (const b of buttonBindings) {
955
1042
  // Perturbation pipeline — discover state-dependent labels
956
1043
  // ---------------------------------------------------------------------------
957
1044
 
958
- const stateDeps = new Map<number, { slotIndex: number; formatExpr: string }>();
1045
+ const stateDeps = new Map<number, { slotIndex: number; formatExpr: string; needsTime?: boolean }>();
959
1046
  const skinDeps = new Map<number, { slotIndex: number; skins: [string, string] }>();
960
1047
 
961
1048
  // Branch info: when tree structure changes between baseline and perturbed,
@@ -1081,6 +1168,8 @@ for (const slot of stateSlots) {
1081
1168
  labelTexts: new Map(),
1082
1169
  rectIdx: 0,
1083
1170
  rectFills: new Map(),
1171
+ elemIdx: 0,
1172
+ elementPositions: new Map(),
1084
1173
  };
1085
1174
  emitNode(appP._root, ctxP, '', null, null);
1086
1175
 
@@ -1099,8 +1188,23 @@ for (const slot of stateSlots) {
1099
1188
  if (String(perturbedValue) === pertText) {
1100
1189
  formatExpr = `"" + this.s${slot.index}`;
1101
1190
  } else if (typeof slot.initialValue === 'boolean') {
1102
- // Boolean state produced different text — emit a ternary
1103
- formatExpr = `this.s${slot.index} ? "${pertText.replace(/"/g, '\\"')}" : "${baseText.replace(/"/g, '\\"')}"`;
1191
+ // Boolean state produced different text — emit a ternary.
1192
+ // If the perturbed text looks like a time format, emit a live
1193
+ // time expression instead of a frozen compile-time snapshot.
1194
+ const pertTimeFmt = inferTimeFormat(pertText, T1);
1195
+ if (pertTimeFmt && (pertTimeFmt === 'MMSS' || pertTimeFmt === 'HHMM')) {
1196
+ // State toggles between static text and live time.
1197
+ // Emit elapsed-time tracking: capture start time when toggled on,
1198
+ // compute elapsed seconds in refresh().
1199
+ formatExpr = `this.s${slot.index} ? (function(e) { return pad(Math.floor(e / 60)) + ":" + pad(e % 60); })(Math.floor((Date.now() - this._startTime_s${slot.index}) / 1000)) : "${baseText.replace(/"/g, '\\"')}"`;
1200
+ stateDeps.set(idx, { slotIndex: slot.index, formatExpr, needsTime: true });
1201
+ process.stderr.write(
1202
+ ` Label ${idx} depends on state slot ${slot.index} (base="${baseText}", perturbed=ELAPSED:${pertTimeFmt})\n`,
1203
+ );
1204
+ continue; // skip the default stateDeps.set below
1205
+ } else {
1206
+ formatExpr = `this.s${slot.index} ? "${pertText.replace(/"/g, '\\"')}" : "${baseText.replace(/"/g, '\\"')}"`;
1207
+ }
1104
1208
  } else {
1105
1209
  formatExpr = `"" + this.s${slot.index}`;
1106
1210
  }
@@ -1242,6 +1346,7 @@ if (listInfo) {
1242
1346
  skins: new Map(), styles: new Map(), declarations: [],
1243
1347
  skinIdx: 0, styleIdx: 0, labelIdx: 0, labelTexts: new Map(),
1244
1348
  rectIdx: 0, rectFills: new Map(),
1349
+ elemIdx: 0, elementPositions: new Map(),
1245
1350
  };
1246
1351
  emitNode(appScroll._root, ctxScroll, '', null, null);
1247
1352
 
@@ -1267,6 +1372,20 @@ if (listInfo) {
1267
1372
  for (const idx of keep) listSlotLabels.add(idx);
1268
1373
  }
1269
1374
 
1375
+ // For message-driven lists without scroll buttons, identify list labels
1376
+ // from the template by matching the expected label count. The last
1377
+ // (visibleCount × labelsPerItem) labels in the T1 render are the list items.
1378
+ if (listSlotLabels.size === 0 && messageInfo && listInfo) {
1379
+ const expectedSlots = listInfo.visibleCount * listInfo.labelsPerItem;
1380
+ const allLabels = [...t1Texts.keys()].sort((a, b) => a - b);
1381
+ // Take the last expectedSlots labels (list items come after headers)
1382
+ const listLabels = allLabels.slice(-expectedSlots);
1383
+ for (const idx of listLabels) {
1384
+ listSlotLabels.add(idx);
1385
+ }
1386
+ process.stderr.write(`Message-driven list labels (inferred): [${[...listSlotLabels].join(', ')}]\n`);
1387
+ }
1388
+
1270
1389
  if (listSlotLabels.size > 0) {
1271
1390
  process.stderr.write(`List slot labels: [${[...listSlotLabels].join(', ')}]\n`);
1272
1391
  }
@@ -1305,6 +1424,8 @@ const ctx2: EmitContext = {
1305
1424
  labelTexts: new Map(),
1306
1425
  rectIdx: 0,
1307
1426
  rectFills: new Map(),
1427
+ elemIdx: 0,
1428
+ elementPositions: new Map(),
1308
1429
  };
1309
1430
  emitNode(app2._root, ctx2, '', null, null);
1310
1431
  const t2Texts = new Map(ctx2.labelTexts);
@@ -1338,6 +1459,105 @@ process.stderr.write(
1338
1459
  .join(', ')}\n`,
1339
1460
  );
1340
1461
 
1462
+ // --- Keyframe pass: detect animated positions/sizes ---
1463
+ // Compare element positions between T1 and T2 to find elements that move.
1464
+ // For those elements, render at 60 time points (one per second over 1 minute)
1465
+ // and build keyframe arrays for position updates in onTimeChanged.
1466
+
1467
+ interface AnimatedElement {
1468
+ elemIdx: number;
1469
+ prop: 'top' | 'width' | 'height' | 'radius';
1470
+ keyframes: number[]; // 60 values (one per second)
1471
+ }
1472
+
1473
+ const animatedElements: AnimatedElement[] = [];
1474
+ const t1Positions = ctx1.elementPositions;
1475
+ const t2Positions = ctx2.elementPositions;
1476
+
1477
+ // Find elements with changed positions between T1 and T2
1478
+ const changedElems = new Set<number>();
1479
+ for (const [idx, pos1] of t1Positions) {
1480
+ const pos2 = t2Positions.get(idx);
1481
+ if (!pos2) continue;
1482
+ if (pos1.top !== pos2.top || pos1.width !== pos2.width ||
1483
+ pos1.height !== pos2.height || (pos1.radius !== undefined && pos1.radius !== pos2.radius)) {
1484
+ changedElems.add(idx);
1485
+ }
1486
+ }
1487
+
1488
+ if (changedElems.size > 0) {
1489
+ process.stderr.write(`Found ${changedElems.size} animated element(s), sampling keyframes...\n`);
1490
+
1491
+ // Sample 60 keyframes (one per second)
1492
+ const keyframeData = new Map<number, Map<string, number[]>>(); // elemIdx → prop → values[]
1493
+
1494
+ for (let s = 0; s < 60; s++) {
1495
+ const mockTime = new OrigDate(T1.getFullYear(), T1.getMonth(), T1.getDate(),
1496
+ T1.getHours(), s, s, 0); // vary both minutes and seconds
1497
+ // Actually set minute = T1.minute, second = s
1498
+ const kfTime = new OrigDate(T1.getFullYear(), T1.getMonth(), T1.getDate(),
1499
+ T1.getHours(), T1.getMinutes(), s, 0);
1500
+
1501
+ (globalThis as unknown as { Date: unknown }).Date = class MockDateKF extends OrigDate {
1502
+ constructor() { super(); return kfTime; }
1503
+ static now() { return kfTime.getTime(); }
1504
+ };
1505
+
1506
+ forcedStateValues.clear();
1507
+ resetStateTracking();
1508
+ silence();
1509
+ const appKF = exampleMain();
1510
+ restore();
1511
+
1512
+ if (appKF) {
1513
+ const ctxKF: EmitContext = {
1514
+ skins: new Map(), styles: new Map(), declarations: [],
1515
+ skinIdx: 0, styleIdx: 0, labelIdx: 0, labelTexts: new Map(),
1516
+ rectIdx: 0, rectFills: new Map(),
1517
+ elemIdx: 0, elementPositions: new Map(),
1518
+ };
1519
+ emitNode(appKF._root, ctxKF, '', null, null);
1520
+
1521
+ for (const eIdx of changedElems) {
1522
+ const pos = ctxKF.elementPositions.get(eIdx);
1523
+ if (!pos) continue;
1524
+ if (!keyframeData.has(eIdx)) keyframeData.set(eIdx, new Map());
1525
+ const props = keyframeData.get(eIdx)!;
1526
+ for (const prop of ['top', 'width', 'height', 'radius'] as const) {
1527
+ const val = pos[prop];
1528
+ if (val === undefined) continue;
1529
+ if (!props.has(prop)) props.set(prop, []);
1530
+ props.get(prop)!.push(val);
1531
+ }
1532
+ }
1533
+ appKF.unmount();
1534
+ }
1535
+ }
1536
+
1537
+ (globalThis as unknown as { Date: typeof Date }).Date = OrigDate;
1538
+
1539
+ // Build AnimatedElement entries for props that actually change
1540
+ for (const [eIdx, props] of keyframeData) {
1541
+ for (const [prop, values] of props) {
1542
+ const allSame = values.every(v => v === values[0]);
1543
+ if (!allSame) {
1544
+ animatedElements.push({
1545
+ elemIdx: eIdx,
1546
+ prop: prop as AnimatedElement['prop'],
1547
+ keyframes: values,
1548
+ });
1549
+ }
1550
+ }
1551
+ }
1552
+
1553
+ process.stderr.write(` Animated properties: ${animatedElements.map(a => `e${a.elemIdx}.${a.prop}`).join(', ')}\n`);
1554
+
1555
+ // Mark elements for naming in the final emit pass
1556
+ for (const ae of animatedElements) {
1557
+ animatedElemNames.add(ae.elemIdx);
1558
+ }
1559
+ }
1560
+
1341
1561
  // --- Final render at T1 for the emitted static snapshot ---
1342
1562
  (globalThis as unknown as { Date: unknown }).Date = class MockDate3 extends OrigDate {
1343
1563
  constructor() {
@@ -1373,6 +1593,8 @@ const ctx: EmitContext = {
1373
1593
  labelTexts: new Map(),
1374
1594
  rectIdx: 0,
1375
1595
  rectFills: new Map(),
1596
+ elemIdx: 0,
1597
+ elementPositions: new Map(),
1376
1598
  };
1377
1599
 
1378
1600
  let contents: string | null;
@@ -1439,8 +1661,10 @@ if (branchInfos.length > 0) {
1439
1661
  }
1440
1662
  contents = branchLines.join(',\n');
1441
1663
  } else {
1442
- // No structural branches — emit the single tree as before
1443
- emitConditionals = true;
1664
+ // No structural branches — emit the single tree as before.
1665
+ // For message-driven apps, skip conditionals since the UI starts with
1666
+ // mock data visible and labels get updated in-place by the Message handler.
1667
+ emitConditionals = !messageInfo;
1444
1668
  contents = emitNode(appFinal._root, ctx, ' ', dynamicLabels, stateDeps, skinDeps);
1445
1669
  }
1446
1670
  appFinal.unmount();
@@ -1467,11 +1691,13 @@ for (const binding of buttonBindings) {
1467
1691
  }
1468
1692
 
1469
1693
  // --- Emit piu output ---
1470
- const hasTimeDeps = dynamicLabels.size > 0;
1694
+ const stateNeedsTime = [...stateDeps.values()].some(d => d.needsTime);
1695
+ const hasAnimatedElements = animatedElements.length > 0;
1696
+ const hasTimeDeps = dynamicLabels.size > 0 || stateNeedsTime || hasAnimatedElements;
1471
1697
  const hasStateDeps = stateDeps.size > 0;
1472
1698
  const hasButtons = buttonActions.length > 0;
1473
1699
  const hasBranches = branchInfos.length > 0;
1474
- const hasConditionals = conditionalChildren.length > 0;
1700
+ const hasConditionals = conditionalChildren.length > 0 && !messageInfo;
1475
1701
  const hasSkinDeps = skinDeps.size > 0;
1476
1702
  const hasList = listInfo !== null && listSlotLabels.size > 0;
1477
1703
  const hasBehavior = hasTimeDeps || hasStateDeps || hasButtons || hasBranches || hasSkinDeps || hasList || hasConditionals;
@@ -1501,6 +1727,14 @@ lines.push(
1501
1727
  '',
1502
1728
  );
1503
1729
 
1730
+ // Emit keyframe arrays for animated elements
1731
+ if (hasAnimatedElements) {
1732
+ for (const ae of animatedElements) {
1733
+ lines.push(`const _kf_e${ae.elemIdx}_${ae.prop} = [${ae.keyframes.join(',')}];`);
1734
+ }
1735
+ lines.push('');
1736
+ }
1737
+
1504
1738
  if (hasTimeDeps) {
1505
1739
  lines.push('function pad(n) { return n < 10 ? "0" + n : "" + n; }');
1506
1740
  lines.push(
@@ -1550,6 +1784,13 @@ if (hasBehavior) {
1550
1784
  lines.push(` this.s${slot.index} = ${JSON.stringify(v)};`);
1551
1785
  }
1552
1786
 
1787
+ // Elapsed-time start markers for state+time hybrid labels
1788
+ for (const [, dep] of stateDeps) {
1789
+ if (dep.needsTime) {
1790
+ lines.push(` this._startTime_s${dep.slotIndex} = Date.now();`);
1791
+ }
1792
+ }
1793
+
1553
1794
  // Time-dependent label refs
1554
1795
  for (const [idx] of labelFormats) {
1555
1796
  lines.push(` this.tl${idx} = c.content("tl${idx}");`);
@@ -1565,6 +1806,12 @@ if (hasBehavior) {
1565
1806
  lines.push(` this.sr${rIdx} = c.content("sr${rIdx}");`);
1566
1807
  }
1567
1808
 
1809
+ // Animated element refs
1810
+ const animElemIndices = [...new Set(animatedElements.map(a => a.elemIdx))];
1811
+ for (const eIdx of animElemIndices) {
1812
+ lines.push(` this.ae${eIdx} = c.content("ae${eIdx}");`);
1813
+ }
1814
+
1568
1815
  // Branch container refs (for structural conditional rendering)
1569
1816
  for (const [si, branches] of branchesBySlot ?? []) {
1570
1817
  for (let bi = 0; bi < branches.length; bi++) {
@@ -1572,8 +1819,8 @@ if (hasBehavior) {
1572
1819
  }
1573
1820
  }
1574
1821
 
1575
- // Per-subtree conditional refs
1576
- for (const cc of conditionalChildren) {
1822
+ // Per-subtree conditional refs (skipped for message-driven apps)
1823
+ if (hasConditionals) for (const cc of conditionalChildren) {
1577
1824
  if (cc.type === 'removed') {
1578
1825
  const name = `cv_s${cc.stateSlot}_${cc.childIndex}`;
1579
1826
  lines.push(` this.${name} = c.content("${name}");`);
@@ -1628,30 +1875,52 @@ if (hasBehavior) {
1628
1875
  lines.push(` if (json) {`);
1629
1876
  lines.push(` try {`);
1630
1877
  lines.push(` _data = JSON.parse(json);`);
1631
- // Toggle: show loaded branches (v0 = baseline = loaded), hide loading (v1)
1632
- for (const [si, branches] of branchesBySlot ?? []) {
1633
- for (let bi = 0; bi < branches.length; bi++) {
1634
- lines.push(` self.br_s${si}_v${bi}.visible = ${bi === 0 ? 'true' : 'false'};`);
1878
+ // Toggle visibility: show loaded content, hide loading state
1879
+ if (branchesBySlot && branchesBySlot.size > 0) {
1880
+ // Structural branches: toggle br_s*_v* containers
1881
+ for (const [si, branches] of branchesBySlot) {
1882
+ for (let bi = 0; bi < branches.length; bi++) {
1883
+ lines.push(` self.br_s${si}_v${bi}.visible = ${bi === 0 ? 'true' : 'false'};`);
1884
+ }
1635
1885
  }
1636
1886
  }
1887
+ // Determine the content root for finding list labels
1888
+ const contentRoot = (branchesBySlot && branchesBySlot.size > 0)
1889
+ ? `self.br_s${[...branchesBySlot.keys()][0]}_v0.first`
1890
+ : 'app.first';
1637
1891
  // Update list labels from parsed data
1638
- if (listInfo && listInfo.labelsPerItem > 1 && listInfo.propertyOrder) {
1892
+ if (listInfo && listInfo.labelsPerItem > 1) {
1639
1893
  const lpi = listInfo.labelsPerItem;
1640
1894
  const vc = listInfo.visibleCount;
1641
- lines.push(` const c = self.br_s${[...branchesBySlot.keys()][0]}_v0.first;`);
1895
+ const props = listInfo.propertyOrder;
1896
+ lines.push(` const c = ${contentRoot};`);
1642
1897
  for (let i = 0; i < vc; i++) {
1643
1898
  lines.push(` const g${i} = c.content("lg${i}");`);
1644
1899
  for (let j = 0; j < lpi; j++) {
1645
- const prop = listInfo.propertyOrder[j]!;
1646
- lines.push(` if (g${i}) { const l = g${i}.content("ls${i}_${j}"); if (l) l.string = _data[${i}] ? _data[${i}].${prop} : ""; }`);
1900
+ // Use property name if known, otherwise access by Object.values
1901
+ const accessor = props ? `_data[${i}].${props[j]}` : `Object.values(_data[${i}] || {})[${j}]`;
1902
+ lines.push(` if (g${i}) { const l = g${i}.content("ls${i}_${j}"); if (l) l.string = _data[${i}] ? ${accessor} || "" : ""; }`);
1647
1903
  }
1648
1904
  }
1649
1905
  } else if (listInfo) {
1650
1906
  const vc = listInfo.visibleCount;
1651
- lines.push(` const c = self.br_s${[...branchesBySlot.keys()][0]}_v0.first;`);
1907
+ lines.push(` const c = ${contentRoot};`);
1652
1908
  for (let i = 0; i < vc; i++) {
1653
1909
  lines.push(` const l${i} = c.content("ls${i}"); if (l${i}) l${i}.string = _data[${i}] || "";`);
1654
1910
  }
1911
+ } else {
1912
+ // Non-list message data: update state-dependent labels
1913
+ // The received data replaces the loading state — toggle conditionals
1914
+ for (const cc of conditionalChildren) {
1915
+ if (cc.type === 'removed') {
1916
+ const name = `cv_s${cc.stateSlot}_${cc.childIndex}`;
1917
+ lines.push(` if (self.${name}) self.${name}.visible = true;`);
1918
+ }
1919
+ }
1920
+ // Update any state-dependent labels with data values
1921
+ for (const [idx, dep] of stateDeps) {
1922
+ lines.push(` if (self.sl${idx}) self.sl${idx}.string = JSON.stringify(_data);`);
1923
+ }
1655
1924
  }
1656
1925
  lines.push(` } catch (e) { console.log("Parse error: " + e.message); }`);
1657
1926
  lines.push(` }`);
@@ -1695,9 +1964,16 @@ if (hasBehavior) {
1695
1964
  case 'reset':
1696
1965
  stmt = `this.s${action.slotIndex} = ${action.value}; this.refresh();`;
1697
1966
  break;
1698
- case 'toggle':
1699
- stmt = `this.s${action.slotIndex} = !this.s${action.slotIndex}; this.refresh();`;
1967
+ case 'toggle': {
1968
+ // Check if any state-dependent label needs elapsed time tracking for this slot
1969
+ const needsElapsed = [...stateDeps.values()].some(d => d.slotIndex === action.slotIndex && d.needsTime);
1970
+ if (needsElapsed) {
1971
+ stmt = `this.s${action.slotIndex} = !this.s${action.slotIndex}; if (this.s${action.slotIndex}) this._startTime_s${action.slotIndex} = Date.now(); this.refresh();`;
1972
+ } else {
1973
+ stmt = `this.s${action.slotIndex} = !this.s${action.slotIndex}; this.refresh();`;
1974
+ }
1700
1975
  break;
1976
+ }
1701
1977
  case 'set_string':
1702
1978
  stmt = `this.s${action.slotIndex} = "${action.stringValue}"; this.refresh();`;
1703
1979
  break;
@@ -1729,8 +2005,8 @@ if (hasBehavior) {
1729
2005
  lines.push(` this.sr${rIdx}.skin = (this.s${dep.slotIndex} !== ${JSON.stringify(slot?.initialValue)}) ? ${pertSkinVar} : ${baseSkinVar};`);
1730
2006
  }
1731
2007
  }
1732
- // Per-subtree conditional visibility
1733
- for (const cc of conditionalChildren) {
2008
+ // Per-subtree conditional visibility (skipped for message-driven apps)
2009
+ if (hasConditionals) for (const cc of conditionalChildren) {
1734
2010
  if (cc.type === 'removed') {
1735
2011
  const name = `cv_s${cc.stateSlot}_${cc.childIndex}`;
1736
2012
  lines.push(` this.${name}.visible = !!this.s${cc.stateSlot};`);
@@ -1769,6 +2045,19 @@ if (hasBehavior) {
1769
2045
  }
1770
2046
  lines.push(` }`);
1771
2047
  }
2048
+ // Animated element position/size updates from keyframe tables
2049
+ if (hasAnimatedElements) {
2050
+ lines.push(' const _s = d.getSeconds();');
2051
+ for (const ae of animatedElements) {
2052
+ const propMap: Record<string, string> = {
2053
+ top: 'y', width: 'width', height: 'height', radius: 'radius',
2054
+ };
2055
+ // piu Content uses .moveBy() or direct property access
2056
+ // For top: use .y property; for width/height: .width/.height
2057
+ const piuProp = propMap[ae.prop] ?? ae.prop;
2058
+ lines.push(` this.ae${ae.elemIdx}.${piuProp} = _kf_e${ae.elemIdx}_${ae.prop}[_s];`);
2059
+ }
2060
+ }
1772
2061
  lines.push(' }');
1773
2062
 
1774
2063
  // refreshList — separate from refresh() for Message-driven data updates
package/scripts/deploy.sh CHANGED
File without changes