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
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Test all examples on the Pebble Alloy emulator with button press verification.
|
|
3
|
+
#
|
|
4
|
+
# For each example:
|
|
5
|
+
# 1. Compile + build + install to emulator
|
|
6
|
+
# 2. Screenshot initial state
|
|
7
|
+
# 3. Send button presses where applicable
|
|
8
|
+
# 4. Screenshot after each button interaction
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# ./scripts/test-emulator.sh # test all examples
|
|
12
|
+
# ./scripts/test-emulator.sh counter # test one example
|
|
13
|
+
# SETTLE_MS=200 ./scripts/test-emulator.sh async-list # with settle delay
|
|
14
|
+
#
|
|
15
|
+
# Screenshots saved to /tmp/react-pebble-emu-test/
|
|
16
|
+
|
|
17
|
+
set -e
|
|
18
|
+
|
|
19
|
+
FILTER="${1:-}"
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
22
|
+
BUILD_DIR="$PROJECT_DIR/.pebble-build"
|
|
23
|
+
SCREENSHOT_DIR="/tmp/react-pebble-emu-test"
|
|
24
|
+
|
|
25
|
+
mkdir -p "$SCREENSHOT_DIR"
|
|
26
|
+
|
|
27
|
+
PASSED=0
|
|
28
|
+
FAILED=0
|
|
29
|
+
TOTAL=0
|
|
30
|
+
|
|
31
|
+
# Deploy an example and take initial screenshot
|
|
32
|
+
deploy_example() {
|
|
33
|
+
local name="$1"
|
|
34
|
+
echo ""
|
|
35
|
+
echo "========================================"
|
|
36
|
+
echo " Testing: $name"
|
|
37
|
+
echo "========================================"
|
|
38
|
+
|
|
39
|
+
cd "$PROJECT_DIR"
|
|
40
|
+
|
|
41
|
+
# Compile
|
|
42
|
+
ENTRY="examples/${name}.tsx" npx vite build --config vite.config.plugin-test.js 2>&1 | grep "\[react-pebble\]" || true
|
|
43
|
+
|
|
44
|
+
# Build
|
|
45
|
+
cd "$BUILD_DIR"
|
|
46
|
+
pebble build 2>&1 | tail -2
|
|
47
|
+
|
|
48
|
+
# Kill any existing emulator session
|
|
49
|
+
pebble kill >/dev/null 2>&1 || true
|
|
50
|
+
sleep 1
|
|
51
|
+
|
|
52
|
+
# Install and wait for app to start
|
|
53
|
+
pebble install --emulator emery --logs > /tmp/react-pebble-emu.log 2>&1 &
|
|
54
|
+
EMU_PID=$!
|
|
55
|
+
sleep 8
|
|
56
|
+
|
|
57
|
+
# Initial screenshot
|
|
58
|
+
pebble screenshot "$SCREENSHOT_DIR/${name}-initial.png" 2>/dev/null || true
|
|
59
|
+
echo " [screenshot] ${name}-initial.png"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Take a named screenshot
|
|
63
|
+
screenshot() {
|
|
64
|
+
local name="$1"
|
|
65
|
+
sleep 1
|
|
66
|
+
pebble screenshot "$SCREENSHOT_DIR/${name}.png" 2>/dev/null || true
|
|
67
|
+
echo " [screenshot] ${name}.png"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Send a button click and wait
|
|
71
|
+
click() {
|
|
72
|
+
local button="$1"
|
|
73
|
+
pebble emu-button click "$button" 2>/dev/null || true
|
|
74
|
+
sleep 0.8
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Clean up emulator after test
|
|
78
|
+
cleanup() {
|
|
79
|
+
pebble kill >/dev/null 2>&1 || true
|
|
80
|
+
kill $EMU_PID 2>/dev/null || true
|
|
81
|
+
wait $EMU_PID 2>/dev/null || true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Test functions for each example
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
test_watchface() {
|
|
89
|
+
deploy_example watchface
|
|
90
|
+
# Watchface has no buttons - just verify it renders
|
|
91
|
+
cleanup
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test_counter() {
|
|
95
|
+
deploy_example counter
|
|
96
|
+
# UP increments, DOWN decrements, SELECT resets
|
|
97
|
+
click up
|
|
98
|
+
screenshot counter-after-up
|
|
99
|
+
click up
|
|
100
|
+
click up
|
|
101
|
+
screenshot counter-after-3up
|
|
102
|
+
click down
|
|
103
|
+
screenshot counter-after-down
|
|
104
|
+
click select
|
|
105
|
+
screenshot counter-after-reset
|
|
106
|
+
cleanup
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test_toggle() {
|
|
110
|
+
deploy_example toggle
|
|
111
|
+
# SELECT toggles on/off
|
|
112
|
+
click select
|
|
113
|
+
screenshot toggle-on
|
|
114
|
+
click select
|
|
115
|
+
screenshot toggle-off
|
|
116
|
+
cleanup
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test_views() {
|
|
120
|
+
deploy_example views
|
|
121
|
+
# SELECT toggles between main and detail view
|
|
122
|
+
click select
|
|
123
|
+
screenshot views-detail
|
|
124
|
+
click select
|
|
125
|
+
screenshot views-main
|
|
126
|
+
cleanup
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
test_multiview() {
|
|
130
|
+
deploy_example multiview
|
|
131
|
+
# UP=settings, DOWN=about, SELECT=home
|
|
132
|
+
click up
|
|
133
|
+
screenshot multiview-settings
|
|
134
|
+
click down
|
|
135
|
+
screenshot multiview-about
|
|
136
|
+
click select
|
|
137
|
+
screenshot multiview-home
|
|
138
|
+
cleanup
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
test_simple_list() {
|
|
142
|
+
deploy_example simple-list
|
|
143
|
+
# DOWN scrolls list down, UP scrolls up
|
|
144
|
+
click down
|
|
145
|
+
screenshot simple-list-scroll1
|
|
146
|
+
click down
|
|
147
|
+
screenshot simple-list-scroll2
|
|
148
|
+
click up
|
|
149
|
+
screenshot simple-list-scrollback
|
|
150
|
+
cleanup
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
test_selected_list() {
|
|
154
|
+
deploy_example selected-list
|
|
155
|
+
# DOWN/UP move selection highlight
|
|
156
|
+
click down
|
|
157
|
+
screenshot selected-list-sel1
|
|
158
|
+
click down
|
|
159
|
+
screenshot selected-list-sel2
|
|
160
|
+
click down
|
|
161
|
+
screenshot selected-list-sel3
|
|
162
|
+
click up
|
|
163
|
+
screenshot selected-list-selback
|
|
164
|
+
cleanup
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
test_rich_list() {
|
|
168
|
+
deploy_example rich-list
|
|
169
|
+
click down
|
|
170
|
+
screenshot rich-list-sel1
|
|
171
|
+
click down
|
|
172
|
+
screenshot rich-list-sel2
|
|
173
|
+
click up
|
|
174
|
+
screenshot rich-list-selback
|
|
175
|
+
cleanup
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
test_tasks() {
|
|
179
|
+
deploy_example tasks
|
|
180
|
+
# DOWN/UP navigate, SELECT goes to detail, BACK returns
|
|
181
|
+
click down
|
|
182
|
+
screenshot tasks-sel1
|
|
183
|
+
click select
|
|
184
|
+
screenshot tasks-detail
|
|
185
|
+
click back
|
|
186
|
+
screenshot tasks-back
|
|
187
|
+
cleanup
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test_jira_lite() {
|
|
191
|
+
deploy_example jira-lite
|
|
192
|
+
# DOWN/UP navigate issues, SELECT opens detail, BACK returns
|
|
193
|
+
click down
|
|
194
|
+
screenshot jira-lite-sel1
|
|
195
|
+
click down
|
|
196
|
+
screenshot jira-lite-sel2
|
|
197
|
+
click select
|
|
198
|
+
screenshot jira-lite-detail
|
|
199
|
+
click back
|
|
200
|
+
screenshot jira-lite-back
|
|
201
|
+
cleanup
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
test_stopwatch() {
|
|
205
|
+
deploy_example stopwatch
|
|
206
|
+
# SELECT starts/stops, DOWN resets
|
|
207
|
+
click select
|
|
208
|
+
sleep 2
|
|
209
|
+
screenshot stopwatch-running
|
|
210
|
+
click select
|
|
211
|
+
screenshot stopwatch-stopped
|
|
212
|
+
click down
|
|
213
|
+
screenshot stopwatch-reset
|
|
214
|
+
cleanup
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
test_nested_cond() {
|
|
218
|
+
deploy_example nested-cond
|
|
219
|
+
# UP toggles header, DOWN toggles footer
|
|
220
|
+
click up
|
|
221
|
+
screenshot nested-cond-no-header
|
|
222
|
+
click down
|
|
223
|
+
screenshot nested-cond-no-footer
|
|
224
|
+
click up
|
|
225
|
+
screenshot nested-cond-both-back
|
|
226
|
+
cleanup
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
test_circles() {
|
|
230
|
+
deploy_example circles
|
|
231
|
+
# No buttons - just verify circles render
|
|
232
|
+
cleanup
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
test_analog_clock() {
|
|
236
|
+
deploy_example analog-clock
|
|
237
|
+
# No buttons - just verify it renders
|
|
238
|
+
cleanup
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
test_dashboard() {
|
|
242
|
+
deploy_example dashboard
|
|
243
|
+
# No buttons - watchface dashboard with battery/connection
|
|
244
|
+
cleanup
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
test_layout_demo() {
|
|
248
|
+
deploy_example layout-demo
|
|
249
|
+
# No buttons - static layout demo
|
|
250
|
+
cleanup
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
test_settings() {
|
|
254
|
+
deploy_example settings
|
|
255
|
+
# UP increases font, DOWN decreases, SELECT cycles theme
|
|
256
|
+
click up
|
|
257
|
+
screenshot settings-font-up
|
|
258
|
+
click up
|
|
259
|
+
screenshot settings-font-up2
|
|
260
|
+
click down
|
|
261
|
+
screenshot settings-font-down
|
|
262
|
+
click select
|
|
263
|
+
screenshot settings-theme1
|
|
264
|
+
click select
|
|
265
|
+
screenshot settings-theme2
|
|
266
|
+
cleanup
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
test_animation() {
|
|
270
|
+
deploy_example animation
|
|
271
|
+
# No buttons - animation runs automatically
|
|
272
|
+
sleep 2
|
|
273
|
+
screenshot animation-mid
|
|
274
|
+
sleep 2
|
|
275
|
+
screenshot animation-late
|
|
276
|
+
cleanup
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
test_compass() {
|
|
280
|
+
deploy_example compass
|
|
281
|
+
# No buttons - compass reads sensors
|
|
282
|
+
# Test accelerometer tap event
|
|
283
|
+
pebble emu-tap --direction x+ 2>/dev/null || true
|
|
284
|
+
sleep 1
|
|
285
|
+
screenshot compass-after-tap
|
|
286
|
+
cleanup
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
test_weather() {
|
|
290
|
+
deploy_example weather
|
|
291
|
+
cleanup
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
test_async_list() {
|
|
295
|
+
SETTLE_MS=200 deploy_example async-list
|
|
296
|
+
click down
|
|
297
|
+
screenshot async-list-scroll
|
|
298
|
+
cleanup
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
test_jira_list() {
|
|
302
|
+
SETTLE_MS=200 deploy_example jira-list
|
|
303
|
+
click down
|
|
304
|
+
screenshot jira-list-sel1
|
|
305
|
+
click select
|
|
306
|
+
screenshot jira-list-detail
|
|
307
|
+
click back
|
|
308
|
+
screenshot jira-list-back
|
|
309
|
+
cleanup
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
# Run tests
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
ALL_EXAMPLES=(
|
|
317
|
+
watchface counter toggle views multiview
|
|
318
|
+
simple-list selected-list rich-list tasks jira-lite
|
|
319
|
+
stopwatch nested-cond circles analog-clock
|
|
320
|
+
dashboard layout-demo settings animation compass
|
|
321
|
+
weather
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Skip async examples by default (need SETTLE_MS)
|
|
325
|
+
ASYNC_EXAMPLES=(async-list jira-list)
|
|
326
|
+
|
|
327
|
+
run_test() {
|
|
328
|
+
local name="$1"
|
|
329
|
+
TOTAL=$((TOTAL + 1))
|
|
330
|
+
|
|
331
|
+
# Convert dashes to underscores for function name
|
|
332
|
+
local fn_name="test_${name//-/_}"
|
|
333
|
+
|
|
334
|
+
if declare -f "$fn_name" > /dev/null 2>&1; then
|
|
335
|
+
if $fn_name 2>&1; then
|
|
336
|
+
echo " [PASS] $name"
|
|
337
|
+
PASSED=$((PASSED + 1))
|
|
338
|
+
else
|
|
339
|
+
echo " [FAIL] $name"
|
|
340
|
+
FAILED=$((FAILED + 1))
|
|
341
|
+
fi
|
|
342
|
+
else
|
|
343
|
+
echo " [SKIP] $name (no test function)"
|
|
344
|
+
fi
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
echo "=== react-pebble emulator test suite ==="
|
|
348
|
+
echo "Screenshots: $SCREENSHOT_DIR"
|
|
349
|
+
echo ""
|
|
350
|
+
|
|
351
|
+
if [ -n "$FILTER" ]; then
|
|
352
|
+
run_test "$FILTER"
|
|
353
|
+
else
|
|
354
|
+
for ex in "${ALL_EXAMPLES[@]}"; do
|
|
355
|
+
run_test "$ex"
|
|
356
|
+
done
|
|
357
|
+
# Also run async examples
|
|
358
|
+
for ex in "${ASYNC_EXAMPLES[@]}"; do
|
|
359
|
+
run_test "$ex"
|
|
360
|
+
done
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
echo ""
|
|
364
|
+
echo "========================================"
|
|
365
|
+
echo " Results: $PASSED passed, $FAILED failed out of $TOTAL"
|
|
366
|
+
echo " Screenshots: $SCREENSHOT_DIR"
|
|
367
|
+
echo "========================================"
|
|
368
|
+
|
|
369
|
+
if [ $FAILED -gt 0 ]; then
|
|
370
|
+
exit 1
|
|
371
|
+
fi
|
package/src/compiler/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface CompileResult {
|
|
|
38
38
|
hasButtons: boolean;
|
|
39
39
|
/** Message keys used by useMessage hooks */
|
|
40
40
|
messageKeys: string[];
|
|
41
|
+
/** Mock data source from useMessage (for generating phone-side JS) */
|
|
42
|
+
mockDataSource: string | null;
|
|
41
43
|
/** Diagnostic messages from the compiler */
|
|
42
44
|
diagnostics: string;
|
|
43
45
|
}
|
|
@@ -51,7 +53,8 @@ export async function compileToPiu(options: CompileOptions): Promise<CompileResu
|
|
|
51
53
|
const log = options.logger ?? (() => {});
|
|
52
54
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
53
55
|
|
|
54
|
-
// Resolve the entry
|
|
56
|
+
// Resolve the entry path — pass the full path so the script can find it
|
|
57
|
+
// whether it's an internal example or an external project file
|
|
55
58
|
const entryPath = resolve(projectRoot, options.entry);
|
|
56
59
|
const exampleName = basename(entryPath).replace(/\.[jt]sx?$/, '');
|
|
57
60
|
|
|
@@ -65,7 +68,7 @@ export async function compileToPiu(options: CompileOptions): Promise<CompileResu
|
|
|
65
68
|
|
|
66
69
|
const env: Record<string, string> = {
|
|
67
70
|
...process.env as Record<string, string>,
|
|
68
|
-
EXAMPLE:
|
|
71
|
+
EXAMPLE: entryPath,
|
|
69
72
|
};
|
|
70
73
|
if (options.settleMs) {
|
|
71
74
|
env.SETTLE_MS = String(options.settleMs);
|
|
@@ -108,7 +111,12 @@ export async function compileToPiu(options: CompileOptions): Promise<CompileResu
|
|
|
108
111
|
const msgMatch = diagnostics.match(/useMessage detected: key="([^"]+)"/);
|
|
109
112
|
if (msgMatch?.[1]) messageKeys.push(msgMatch[1]);
|
|
110
113
|
|
|
114
|
+
// Extract mock data source for phone-side JS generation
|
|
115
|
+
let mockDataSource: string | null = null;
|
|
116
|
+
const mockMatch = diagnostics.match(/mockDataValue=([\s\S]*?)(?:\n[A-Z]|\n$)/);
|
|
117
|
+
if (mockMatch?.[1]) mockDataSource = mockMatch[1].trim();
|
|
118
|
+
|
|
111
119
|
log(`Compiled ${exampleName}: ${code.split('\n').length} lines, buttons=${hasButtons}, messageKeys=[${messageKeys.join(',')}]`);
|
|
112
120
|
|
|
113
|
-
return { code, hasButtons, messageKeys, diagnostics };
|
|
121
|
+
return { code, hasButtons, messageKeys, mockDataSource, diagnostics };
|
|
114
122
|
}
|
package/src/components/index.tsx
CHANGED
|
@@ -107,6 +107,7 @@ export interface RectProps extends PositionProps, SizeProps {
|
|
|
107
107
|
fill?: ColorName;
|
|
108
108
|
stroke?: ColorName;
|
|
109
109
|
strokeWidth?: number;
|
|
110
|
+
borderRadius?: number;
|
|
110
111
|
children?: ReactNode;
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -151,13 +152,17 @@ export function Line(props: LineProps) {
|
|
|
151
152
|
|
|
152
153
|
export interface ImageProps extends PositionProps {
|
|
153
154
|
bitmap: unknown;
|
|
155
|
+
/** Rotation in radians. */
|
|
156
|
+
rotation?: number;
|
|
157
|
+
/** Scale factor (1 = original size). */
|
|
158
|
+
scale?: number;
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
export function Image(props: ImageProps) {
|
|
157
162
|
return React.createElement('pbl-image', props);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
export interface GroupProps extends PositionProps {
|
|
165
|
+
export interface GroupProps extends PositionProps, SizeProps {
|
|
161
166
|
children?: ReactNode;
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -165,6 +170,75 @@ export function Group({ children, ...props }: GroupProps) {
|
|
|
165
170
|
return React.createElement('pbl-group', props, children);
|
|
166
171
|
}
|
|
167
172
|
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Flow layout containers
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
export interface ColumnProps extends PositionProps, SizeProps {
|
|
178
|
+
/** Gap between children in pixels (default 0). */
|
|
179
|
+
gap?: number;
|
|
180
|
+
children?: ReactNode;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Stacks children vertically, auto-computing each child's `y` offset.
|
|
185
|
+
* Children should specify `h` (or `height`) for correct stacking;
|
|
186
|
+
* children without a height are given a default of 20px.
|
|
187
|
+
*/
|
|
188
|
+
export function Column({ x = 0, y = 0, gap = 0, children, ...props }: ColumnProps) {
|
|
189
|
+
let offsetY = 0;
|
|
190
|
+
const mapped = toArray(children).map((child, i) => {
|
|
191
|
+
if (!child || typeof child !== 'object') return child;
|
|
192
|
+
const vnode = child as { type: unknown; props: Record<string, unknown> };
|
|
193
|
+
if (!vnode.type || !vnode.props) return child;
|
|
194
|
+
const childH = (vnode.props.h ?? vnode.props.height ?? 20) as number;
|
|
195
|
+
const el = React.createElement(
|
|
196
|
+
vnode.type as string,
|
|
197
|
+
{ ...(vnode.props as object), y: offsetY, key: i },
|
|
198
|
+
);
|
|
199
|
+
offsetY += childH + gap;
|
|
200
|
+
return el;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return React.createElement('pbl-group', { x, y, ...props }, ...(mapped as ReactNode[]));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface RowProps extends PositionProps, SizeProps {
|
|
207
|
+
/** Gap between children in pixels (default 0). */
|
|
208
|
+
gap?: number;
|
|
209
|
+
children?: ReactNode;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Stacks children horizontally, auto-computing each child's `x` offset.
|
|
214
|
+
* Children should specify `w` (or `width`) for correct stacking;
|
|
215
|
+
* children without a width are given a default of 40px.
|
|
216
|
+
*/
|
|
217
|
+
export function Row({ x = 0, y = 0, gap = 0, children, ...props }: RowProps) {
|
|
218
|
+
let offsetX = 0;
|
|
219
|
+
const mapped = toArray(children).map((child, i) => {
|
|
220
|
+
if (!child || typeof child !== 'object') return child;
|
|
221
|
+
const vnode = child as { type: unknown; props: Record<string, unknown> };
|
|
222
|
+
if (!vnode.type || !vnode.props) return child;
|
|
223
|
+
const childW = (vnode.props.w ?? vnode.props.width ?? 40) as number;
|
|
224
|
+
const el = React.createElement(
|
|
225
|
+
vnode.type as string,
|
|
226
|
+
{ ...(vnode.props as object), x: offsetX, key: i },
|
|
227
|
+
);
|
|
228
|
+
offsetX += childW + gap;
|
|
229
|
+
return el;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return React.createElement('pbl-group', { x, y, ...props }, ...(mapped as ReactNode[]));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Flatten ComponentChildren into an array, filtering nulls. */
|
|
236
|
+
function toArray(children: ReactNode): unknown[] {
|
|
237
|
+
if (children == null) return [];
|
|
238
|
+
if (Array.isArray(children)) return children.filter(Boolean);
|
|
239
|
+
return [children];
|
|
240
|
+
}
|
|
241
|
+
|
|
168
242
|
export interface StatusBarProps {
|
|
169
243
|
color?: ColorName;
|
|
170
244
|
backgroundColor?: ColorName;
|