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.
- package/LICENSE +21 -0
- package/dist/lib/compiler.cjs +2 -2
- package/dist/lib/compiler.cjs.map +1 -1
- package/dist/lib/compiler.js +4 -1
- package/dist/lib/compiler.js.map +1 -1
- package/dist/lib/components.cjs +1 -1
- package/dist/lib/components.cjs.map +1 -1
- package/dist/lib/components.js +44 -5
- package/dist/lib/components.js.map +1 -1
- package/dist/lib/hooks.cjs +1 -1
- package/dist/lib/hooks.cjs.map +1 -1
- package/dist/lib/hooks.js +198 -3
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.js +231 -108
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/plugin.cjs +25 -5
- package/dist/lib/plugin.cjs.map +1 -1
- package/dist/lib/plugin.js +62 -35
- package/dist/lib/plugin.js.map +1 -1
- package/dist/lib/src/compiler/index.d.ts +2 -0
- package/dist/lib/src/components/index.d.ts +28 -1
- package/dist/lib/src/hooks/index.d.ts +182 -0
- package/dist/lib/src/index.d.ts +4 -4
- package/dist/lib/src/pebble-output.d.ts +15 -0
- package/dist/lib/src/plugin/index.d.ts +6 -0
- package/package.json +10 -11
- package/scripts/compile-to-piu.ts +315 -26
- package/scripts/deploy.sh +0 -0
- package/scripts/test-emulator.sh +371 -0
- package/src/compiler/index.ts +8 -1
- package/src/components/index.tsx +75 -1
- package/src/hooks/index.ts +507 -19
- package/src/index.ts +26 -0
- package/src/pebble-output.ts +408 -48
- package/src/plugin/index.ts +101 -49
- package/src/types/moddable.d.ts +26 -4
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
|
1892
|
+
if (listInfo && listInfo.labelsPerItem > 1) {
|
|
1639
1893
|
const lpi = listInfo.labelsPerItem;
|
|
1640
1894
|
const vc = listInfo.visibleCount;
|
|
1641
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|