react-pebble 0.1.0 → 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 +21 -18
- 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 +346 -35
- package/scripts/deploy.sh +0 -0
- package/scripts/test-emulator.sh +371 -0
- package/src/compiler/index.ts +11 -3
- 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
|
@@ -36,18 +36,31 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
36
36
|
// Dynamic example import
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const exampleInput = process.env.EXAMPLE ?? 'watchface';
|
|
40
40
|
const settleMs = Number(process.env.SETTLE_MS ?? '0');
|
|
41
41
|
const platform = process.env.PEBBLE_PLATFORM ?? 'emery';
|
|
42
42
|
const settle = () =>
|
|
43
43
|
settleMs > 0 ? new Promise<void>((r) => setTimeout(r, settleMs)) : Promise.resolve();
|
|
44
44
|
|
|
45
45
|
// Set platform screen dimensions before importing the example
|
|
46
|
-
// (so SCREEN.width/height are correct when the component renders)
|
|
47
46
|
import { _setPlatform, SCREEN } from '../src/platform.js';
|
|
48
47
|
_setPlatform(platform);
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
// Resolve the entry: could be a bare name (e.g., "watchface") for internal examples,
|
|
50
|
+
// or an absolute/relative path (e.g., "/tmp/my-app/src/App.tsx") for external projects.
|
|
51
|
+
let entryPath: string;
|
|
52
|
+
let exampleName: string;
|
|
53
|
+
if (exampleInput.includes('/') || exampleInput.includes('\\')) {
|
|
54
|
+
// Absolute or relative path — resolve from cwd
|
|
55
|
+
entryPath = resolve(exampleInput);
|
|
56
|
+
exampleName = entryPath.replace(/\.[jt]sx?$/, '').split('/').pop()!;
|
|
57
|
+
} else {
|
|
58
|
+
// Bare name — look in ../examples/
|
|
59
|
+
entryPath = resolve(__dirname, '..', 'examples', `${exampleInput}.tsx`);
|
|
60
|
+
exampleName = exampleInput;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const exampleMod = await import(entryPath);
|
|
51
64
|
const exampleMain: (...args: unknown[]) => ReturnType<typeof render> =
|
|
52
65
|
exampleMod.main ?? exampleMod.default;
|
|
53
66
|
|
|
@@ -65,6 +78,7 @@ function colorToHex(name: string): string {
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
const FONT_TO_PIU: Record<string, string> = {
|
|
81
|
+
// Gothic family
|
|
68
82
|
gothic14: '14px Gothic',
|
|
69
83
|
gothic14Bold: 'bold 14px Gothic',
|
|
70
84
|
gothic18: '18px Gothic',
|
|
@@ -73,9 +87,25 @@ const FONT_TO_PIU: Record<string, string> = {
|
|
|
73
87
|
gothic24Bold: 'bold 24px Gothic',
|
|
74
88
|
gothic28: '28px Gothic',
|
|
75
89
|
gothic28Bold: 'bold 28px Gothic',
|
|
90
|
+
// Bitham family
|
|
76
91
|
bitham30Black: 'black 30px Bitham',
|
|
77
92
|
bitham42Bold: 'bold 42px Bitham',
|
|
78
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',
|
|
79
109
|
};
|
|
80
110
|
|
|
81
111
|
function fontToPiu(name: string | undefined): string {
|
|
@@ -165,8 +195,17 @@ const buttonBindings: ButtonBinding[] = [];
|
|
|
165
195
|
* Parse an example source file into a TypeScript AST SourceFile.
|
|
166
196
|
*/
|
|
167
197
|
function parseExampleSource(exName: string): ts.SourceFile | null {
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
// If exName is an absolute path, try it directly
|
|
199
|
+
if (exName.startsWith('/')) {
|
|
200
|
+
try {
|
|
201
|
+
const source = readFileSync(exName, 'utf-8');
|
|
202
|
+
return ts.createSourceFile(exName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
203
|
+
} catch { /* fall through to extension search */ }
|
|
204
|
+
}
|
|
205
|
+
for (const ext of ['.tsx', '.ts', '.jsx', '']) {
|
|
206
|
+
const srcPath = exName.startsWith('/')
|
|
207
|
+
? `${exName}${ext}`
|
|
208
|
+
: resolve(__dirname, '..', 'examples', `${exName}${ext}`);
|
|
170
209
|
try {
|
|
171
210
|
const source = readFileSync(srcPath, 'utf-8');
|
|
172
211
|
return ts.createSourceFile(srcPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
@@ -248,8 +287,8 @@ function buildSetterSlotMap(exName: string): Map<string, number> {
|
|
|
248
287
|
return map;
|
|
249
288
|
}
|
|
250
289
|
|
|
251
|
-
const setterSlotMap = buildSetterSlotMap(
|
|
252
|
-
const listInfo = detectListPatterns(
|
|
290
|
+
const setterSlotMap = buildSetterSlotMap(entryPath);
|
|
291
|
+
const listInfo = detectListPatterns(entryPath);
|
|
253
292
|
if (listInfo) {
|
|
254
293
|
process.stderr.write(`List detected: array="${listInfo.dataArrayName}" visible=${listInfo.visibleCount} labelsPerItem=${listInfo.labelsPerItem}\n`);
|
|
255
294
|
if (listInfo.dataArrayValues) process.stderr.write(` values: ${JSON.stringify(listInfo.dataArrayValues)}\n`);
|
|
@@ -298,9 +337,27 @@ function detectUseMessage(exName: string): MessageInfo | null {
|
|
|
298
337
|
return { key, mockDataArrayName };
|
|
299
338
|
}
|
|
300
339
|
|
|
301
|
-
const messageInfo = detectUseMessage(
|
|
340
|
+
const messageInfo = detectUseMessage(entryPath);
|
|
302
341
|
if (messageInfo) {
|
|
303
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
|
+
}
|
|
304
361
|
}
|
|
305
362
|
|
|
306
363
|
/** Module-level map collecting string enum values per slot from handler analysis */
|
|
@@ -385,6 +442,32 @@ function analyzeSetterCall(call: ts.CallExpression, sf: ts.SourceFile): HandlerA
|
|
|
385
442
|
if (ts.isPrefixUnaryExpression(body) && body.operator === ts.SyntaxKind.ExclamationToken) {
|
|
386
443
|
return { type: 'toggle', slotIndex, value: 0 };
|
|
387
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
|
+
}
|
|
388
471
|
}
|
|
389
472
|
|
|
390
473
|
return null;
|
|
@@ -564,6 +647,15 @@ function detectListPatterns(exName: string): ListInfo | null {
|
|
|
564
647
|
// Emit context
|
|
565
648
|
// ---------------------------------------------------------------------------
|
|
566
649
|
|
|
650
|
+
interface ElementPos {
|
|
651
|
+
type: string;
|
|
652
|
+
left: number;
|
|
653
|
+
top: number;
|
|
654
|
+
width: number;
|
|
655
|
+
height: number;
|
|
656
|
+
radius?: number;
|
|
657
|
+
}
|
|
658
|
+
|
|
567
659
|
interface EmitContext {
|
|
568
660
|
skins: Map<string, string>;
|
|
569
661
|
styles: Map<string, string>;
|
|
@@ -576,6 +668,10 @@ interface EmitContext {
|
|
|
576
668
|
labelTexts: Map<number, string>;
|
|
577
669
|
/** Map from rect sequential index → its fill color name */
|
|
578
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>;
|
|
579
675
|
}
|
|
580
676
|
|
|
581
677
|
function ensureSkin(ctx: EmitContext, fill: string): string {
|
|
@@ -610,6 +706,7 @@ interface ConditionalChild {
|
|
|
610
706
|
}
|
|
611
707
|
const conditionalChildren: ConditionalChild[] = [];
|
|
612
708
|
let emitConditionals = false; // Only wrap conditionals during final emit
|
|
709
|
+
const animatedElemNames = new Set<number>(); // Element indices that need names for animation
|
|
613
710
|
let conditionalDepth = 0; // Track nesting — only wrap at depth 1 (root Group's children)
|
|
614
711
|
|
|
615
712
|
// Emit piu tree
|
|
@@ -692,10 +789,14 @@ function emitNode(
|
|
|
692
789
|
// Track rect fill for skin reactivity detection
|
|
693
790
|
const rectIdx = ctx.rectIdx++;
|
|
694
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 });
|
|
695
795
|
|
|
696
|
-
// 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
|
|
697
797
|
const isSkinDynamic = skinDeps?.has(rectIdx) ?? false;
|
|
698
|
-
const
|
|
798
|
+
const isAnimated = animatedElemNames.has(rEIdx);
|
|
799
|
+
const nameProp = isSkinDynamic ? `, name: "sr${rectIdx}"` : isAnimated ? `, name: "ae${rEIdx}"` : '';
|
|
699
800
|
|
|
700
801
|
// Use constraint-based layout when dimensions match screen size
|
|
701
802
|
// (so the output adapts to any screen at runtime)
|
|
@@ -779,7 +880,10 @@ function emitNode(
|
|
|
779
880
|
const skinVar = ensureSkin(ctx, fill);
|
|
780
881
|
// piu RoundRect with radius = r draws a circle when width = height = 2*r
|
|
781
882
|
const size = r * 2;
|
|
782
|
-
|
|
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} })`;
|
|
783
887
|
}
|
|
784
888
|
|
|
785
889
|
default:
|
|
@@ -827,7 +931,7 @@ function buildSizeProps(x: number, y: number, w: number, h: number): string {
|
|
|
827
931
|
// Time format inference
|
|
828
932
|
// ---------------------------------------------------------------------------
|
|
829
933
|
|
|
830
|
-
type TimeFormat = 'HHMM' | 'SS' | 'DATE';
|
|
934
|
+
type TimeFormat = 'HHMM' | 'MMSS' | 'SS' | 'DATE';
|
|
831
935
|
|
|
832
936
|
function inferTimeFormat(textAtT1: string, t1: Date): TimeFormat | null {
|
|
833
937
|
const hh = pad2(t1.getHours());
|
|
@@ -840,6 +944,7 @@ function inferTimeFormat(textAtT1: string, t1: Date): TimeFormat | null {
|
|
|
840
944
|
];
|
|
841
945
|
|
|
842
946
|
if (textAtT1 === `${hh}:${mm}`) return 'HHMM';
|
|
947
|
+
if (textAtT1 === `${mm}:${ss}`) return 'MMSS';
|
|
843
948
|
if (textAtT1 === ss) return 'SS';
|
|
844
949
|
if (
|
|
845
950
|
textAtT1.includes(days[t1.getDay()]!) &&
|
|
@@ -854,6 +959,8 @@ function emitTimeExpr(fmt: TimeFormat): string {
|
|
|
854
959
|
switch (fmt) {
|
|
855
960
|
case 'HHMM':
|
|
856
961
|
return 'pad(d.getHours()) + ":" + pad(d.getMinutes())';
|
|
962
|
+
case 'MMSS':
|
|
963
|
+
return 'pad(d.getMinutes()) + ":" + pad(d.getSeconds())';
|
|
857
964
|
case 'SS':
|
|
858
965
|
return 'pad(d.getSeconds())';
|
|
859
966
|
case 'DATE':
|
|
@@ -879,7 +986,7 @@ let listScrollSlotIndex = -1;
|
|
|
879
986
|
|
|
880
987
|
// Install interceptors BEFORE any rendering
|
|
881
988
|
installUseStateInterceptor();
|
|
882
|
-
extractButtonBindingsFromSource(
|
|
989
|
+
extractButtonBindingsFromSource(entryPath);
|
|
883
990
|
|
|
884
991
|
// Create BOTH test dates with the REAL Date before any mocking.
|
|
885
992
|
const OrigDate = globalThis.Date;
|
|
@@ -918,6 +1025,8 @@ const ctx1: EmitContext = {
|
|
|
918
1025
|
labelTexts: new Map(),
|
|
919
1026
|
rectIdx: 0,
|
|
920
1027
|
rectFills: new Map(),
|
|
1028
|
+
elemIdx: 0,
|
|
1029
|
+
elementPositions: new Map(),
|
|
921
1030
|
};
|
|
922
1031
|
emitNode(app1._root, ctx1, '', null, null);
|
|
923
1032
|
const t1Texts = new Map(ctx1.labelTexts);
|
|
@@ -933,7 +1042,7 @@ for (const b of buttonBindings) {
|
|
|
933
1042
|
// Perturbation pipeline — discover state-dependent labels
|
|
934
1043
|
// ---------------------------------------------------------------------------
|
|
935
1044
|
|
|
936
|
-
const stateDeps = new Map<number, { slotIndex: number; formatExpr: string }>();
|
|
1045
|
+
const stateDeps = new Map<number, { slotIndex: number; formatExpr: string; needsTime?: boolean }>();
|
|
937
1046
|
const skinDeps = new Map<number, { slotIndex: number; skins: [string, string] }>();
|
|
938
1047
|
|
|
939
1048
|
// Branch info: when tree structure changes between baseline and perturbed,
|
|
@@ -1059,6 +1168,8 @@ for (const slot of stateSlots) {
|
|
|
1059
1168
|
labelTexts: new Map(),
|
|
1060
1169
|
rectIdx: 0,
|
|
1061
1170
|
rectFills: new Map(),
|
|
1171
|
+
elemIdx: 0,
|
|
1172
|
+
elementPositions: new Map(),
|
|
1062
1173
|
};
|
|
1063
1174
|
emitNode(appP._root, ctxP, '', null, null);
|
|
1064
1175
|
|
|
@@ -1077,8 +1188,23 @@ for (const slot of stateSlots) {
|
|
|
1077
1188
|
if (String(perturbedValue) === pertText) {
|
|
1078
1189
|
formatExpr = `"" + this.s${slot.index}`;
|
|
1079
1190
|
} else if (typeof slot.initialValue === 'boolean') {
|
|
1080
|
-
// Boolean state produced different text — emit a ternary
|
|
1081
|
-
|
|
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
|
+
}
|
|
1082
1208
|
} else {
|
|
1083
1209
|
formatExpr = `"" + this.s${slot.index}`;
|
|
1084
1210
|
}
|
|
@@ -1220,6 +1346,7 @@ if (listInfo) {
|
|
|
1220
1346
|
skins: new Map(), styles: new Map(), declarations: [],
|
|
1221
1347
|
skinIdx: 0, styleIdx: 0, labelIdx: 0, labelTexts: new Map(),
|
|
1222
1348
|
rectIdx: 0, rectFills: new Map(),
|
|
1349
|
+
elemIdx: 0, elementPositions: new Map(),
|
|
1223
1350
|
};
|
|
1224
1351
|
emitNode(appScroll._root, ctxScroll, '', null, null);
|
|
1225
1352
|
|
|
@@ -1245,6 +1372,20 @@ if (listInfo) {
|
|
|
1245
1372
|
for (const idx of keep) listSlotLabels.add(idx);
|
|
1246
1373
|
}
|
|
1247
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
|
+
|
|
1248
1389
|
if (listSlotLabels.size > 0) {
|
|
1249
1390
|
process.stderr.write(`List slot labels: [${[...listSlotLabels].join(', ')}]\n`);
|
|
1250
1391
|
}
|
|
@@ -1283,6 +1424,8 @@ const ctx2: EmitContext = {
|
|
|
1283
1424
|
labelTexts: new Map(),
|
|
1284
1425
|
rectIdx: 0,
|
|
1285
1426
|
rectFills: new Map(),
|
|
1427
|
+
elemIdx: 0,
|
|
1428
|
+
elementPositions: new Map(),
|
|
1286
1429
|
};
|
|
1287
1430
|
emitNode(app2._root, ctx2, '', null, null);
|
|
1288
1431
|
const t2Texts = new Map(ctx2.labelTexts);
|
|
@@ -1316,6 +1459,105 @@ process.stderr.write(
|
|
|
1316
1459
|
.join(', ')}\n`,
|
|
1317
1460
|
);
|
|
1318
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
|
+
|
|
1319
1561
|
// --- Final render at T1 for the emitted static snapshot ---
|
|
1320
1562
|
(globalThis as unknown as { Date: unknown }).Date = class MockDate3 extends OrigDate {
|
|
1321
1563
|
constructor() {
|
|
@@ -1351,6 +1593,8 @@ const ctx: EmitContext = {
|
|
|
1351
1593
|
labelTexts: new Map(),
|
|
1352
1594
|
rectIdx: 0,
|
|
1353
1595
|
rectFills: new Map(),
|
|
1596
|
+
elemIdx: 0,
|
|
1597
|
+
elementPositions: new Map(),
|
|
1354
1598
|
};
|
|
1355
1599
|
|
|
1356
1600
|
let contents: string | null;
|
|
@@ -1417,8 +1661,10 @@ if (branchInfos.length > 0) {
|
|
|
1417
1661
|
}
|
|
1418
1662
|
contents = branchLines.join(',\n');
|
|
1419
1663
|
} else {
|
|
1420
|
-
// No structural branches — emit the single tree as before
|
|
1421
|
-
|
|
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;
|
|
1422
1668
|
contents = emitNode(appFinal._root, ctx, ' ', dynamicLabels, stateDeps, skinDeps);
|
|
1423
1669
|
}
|
|
1424
1670
|
appFinal.unmount();
|
|
@@ -1445,11 +1691,13 @@ for (const binding of buttonBindings) {
|
|
|
1445
1691
|
}
|
|
1446
1692
|
|
|
1447
1693
|
// --- Emit piu output ---
|
|
1448
|
-
const
|
|
1694
|
+
const stateNeedsTime = [...stateDeps.values()].some(d => d.needsTime);
|
|
1695
|
+
const hasAnimatedElements = animatedElements.length > 0;
|
|
1696
|
+
const hasTimeDeps = dynamicLabels.size > 0 || stateNeedsTime || hasAnimatedElements;
|
|
1449
1697
|
const hasStateDeps = stateDeps.size > 0;
|
|
1450
1698
|
const hasButtons = buttonActions.length > 0;
|
|
1451
1699
|
const hasBranches = branchInfos.length > 0;
|
|
1452
|
-
const hasConditionals = conditionalChildren.length > 0;
|
|
1700
|
+
const hasConditionals = conditionalChildren.length > 0 && !messageInfo;
|
|
1453
1701
|
const hasSkinDeps = skinDeps.size > 0;
|
|
1454
1702
|
const hasList = listInfo !== null && listSlotLabels.size > 0;
|
|
1455
1703
|
const hasBehavior = hasTimeDeps || hasStateDeps || hasButtons || hasBranches || hasSkinDeps || hasList || hasConditionals;
|
|
@@ -1479,6 +1727,14 @@ lines.push(
|
|
|
1479
1727
|
'',
|
|
1480
1728
|
);
|
|
1481
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
|
+
|
|
1482
1738
|
if (hasTimeDeps) {
|
|
1483
1739
|
lines.push('function pad(n) { return n < 10 ? "0" + n : "" + n; }');
|
|
1484
1740
|
lines.push(
|
|
@@ -1528,6 +1784,13 @@ if (hasBehavior) {
|
|
|
1528
1784
|
lines.push(` this.s${slot.index} = ${JSON.stringify(v)};`);
|
|
1529
1785
|
}
|
|
1530
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
|
+
|
|
1531
1794
|
// Time-dependent label refs
|
|
1532
1795
|
for (const [idx] of labelFormats) {
|
|
1533
1796
|
lines.push(` this.tl${idx} = c.content("tl${idx}");`);
|
|
@@ -1543,6 +1806,12 @@ if (hasBehavior) {
|
|
|
1543
1806
|
lines.push(` this.sr${rIdx} = c.content("sr${rIdx}");`);
|
|
1544
1807
|
}
|
|
1545
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
|
+
|
|
1546
1815
|
// Branch container refs (for structural conditional rendering)
|
|
1547
1816
|
for (const [si, branches] of branchesBySlot ?? []) {
|
|
1548
1817
|
for (let bi = 0; bi < branches.length; bi++) {
|
|
@@ -1550,8 +1819,8 @@ if (hasBehavior) {
|
|
|
1550
1819
|
}
|
|
1551
1820
|
}
|
|
1552
1821
|
|
|
1553
|
-
// Per-subtree conditional refs
|
|
1554
|
-
for (const cc of conditionalChildren) {
|
|
1822
|
+
// Per-subtree conditional refs (skipped for message-driven apps)
|
|
1823
|
+
if (hasConditionals) for (const cc of conditionalChildren) {
|
|
1555
1824
|
if (cc.type === 'removed') {
|
|
1556
1825
|
const name = `cv_s${cc.stateSlot}_${cc.childIndex}`;
|
|
1557
1826
|
lines.push(` this.${name} = c.content("${name}");`);
|
|
@@ -1606,30 +1875,52 @@ if (hasBehavior) {
|
|
|
1606
1875
|
lines.push(` if (json) {`);
|
|
1607
1876
|
lines.push(` try {`);
|
|
1608
1877
|
lines.push(` _data = JSON.parse(json);`);
|
|
1609
|
-
// Toggle: show loaded
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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
|
+
}
|
|
1613
1885
|
}
|
|
1614
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';
|
|
1615
1891
|
// Update list labels from parsed data
|
|
1616
|
-
if (listInfo && listInfo.labelsPerItem > 1
|
|
1892
|
+
if (listInfo && listInfo.labelsPerItem > 1) {
|
|
1617
1893
|
const lpi = listInfo.labelsPerItem;
|
|
1618
1894
|
const vc = listInfo.visibleCount;
|
|
1619
|
-
|
|
1895
|
+
const props = listInfo.propertyOrder;
|
|
1896
|
+
lines.push(` const c = ${contentRoot};`);
|
|
1620
1897
|
for (let i = 0; i < vc; i++) {
|
|
1621
1898
|
lines.push(` const g${i} = c.content("lg${i}");`);
|
|
1622
1899
|
for (let j = 0; j < lpi; j++) {
|
|
1623
|
-
|
|
1624
|
-
|
|
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} || "" : ""; }`);
|
|
1625
1903
|
}
|
|
1626
1904
|
}
|
|
1627
1905
|
} else if (listInfo) {
|
|
1628
1906
|
const vc = listInfo.visibleCount;
|
|
1629
|
-
lines.push(` const c =
|
|
1907
|
+
lines.push(` const c = ${contentRoot};`);
|
|
1630
1908
|
for (let i = 0; i < vc; i++) {
|
|
1631
1909
|
lines.push(` const l${i} = c.content("ls${i}"); if (l${i}) l${i}.string = _data[${i}] || "";`);
|
|
1632
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
|
+
}
|
|
1633
1924
|
}
|
|
1634
1925
|
lines.push(` } catch (e) { console.log("Parse error: " + e.message); }`);
|
|
1635
1926
|
lines.push(` }`);
|
|
@@ -1673,9 +1964,16 @@ if (hasBehavior) {
|
|
|
1673
1964
|
case 'reset':
|
|
1674
1965
|
stmt = `this.s${action.slotIndex} = ${action.value}; this.refresh();`;
|
|
1675
1966
|
break;
|
|
1676
|
-
case 'toggle':
|
|
1677
|
-
|
|
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
|
+
}
|
|
1678
1975
|
break;
|
|
1976
|
+
}
|
|
1679
1977
|
case 'set_string':
|
|
1680
1978
|
stmt = `this.s${action.slotIndex} = "${action.stringValue}"; this.refresh();`;
|
|
1681
1979
|
break;
|
|
@@ -1707,8 +2005,8 @@ if (hasBehavior) {
|
|
|
1707
2005
|
lines.push(` this.sr${rIdx}.skin = (this.s${dep.slotIndex} !== ${JSON.stringify(slot?.initialValue)}) ? ${pertSkinVar} : ${baseSkinVar};`);
|
|
1708
2006
|
}
|
|
1709
2007
|
}
|
|
1710
|
-
// Per-subtree conditional visibility
|
|
1711
|
-
for (const cc of conditionalChildren) {
|
|
2008
|
+
// Per-subtree conditional visibility (skipped for message-driven apps)
|
|
2009
|
+
if (hasConditionals) for (const cc of conditionalChildren) {
|
|
1712
2010
|
if (cc.type === 'removed') {
|
|
1713
2011
|
const name = `cv_s${cc.stateSlot}_${cc.childIndex}`;
|
|
1714
2012
|
lines.push(` this.${name}.visible = !!this.s${cc.stateSlot};`);
|
|
@@ -1747,6 +2045,19 @@ if (hasBehavior) {
|
|
|
1747
2045
|
}
|
|
1748
2046
|
lines.push(` }`);
|
|
1749
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
|
+
}
|
|
1750
2061
|
lines.push(' }');
|
|
1751
2062
|
|
|
1752
2063
|
// refreshList — separate from refresh() for Message-driven data updates
|
package/scripts/deploy.sh
CHANGED
|
File without changes
|