sdui-web 0.4.0 → 0.6.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.
@@ -0,0 +1,908 @@
1
+ // src/react/SduiRenderer.tsx
2
+ import { useEffect, useRef, useState } from "react";
3
+
4
+ // src/utils.ts
5
+ function resolveColor(color, theme) {
6
+ if (!color) return void 0;
7
+ if (theme === "dark") {
8
+ return color.dark ?? color.light;
9
+ }
10
+ return color.light ?? color.dark;
11
+ }
12
+ function resolveSize(size) {
13
+ if (!size || !size.unit) return void 0;
14
+ switch (size.unit) {
15
+ case "dp":
16
+ return `${size.value ?? 0}px`;
17
+ case "match_parent":
18
+ return "100%";
19
+ case "wrap_content":
20
+ return "fit-content";
21
+ default:
22
+ return void 0;
23
+ }
24
+ }
25
+ function resolveSpacing(spacing) {
26
+ if (!spacing) return {};
27
+ return {
28
+ top: spacing.top != null ? `${spacing.top}px` : void 0,
29
+ bottom: spacing.bottom != null ? `${spacing.bottom}px` : void 0,
30
+ left: spacing.start != null ? `${spacing.start}px` : void 0,
31
+ right: spacing.end != null ? `${spacing.end}px` : void 0
32
+ };
33
+ }
34
+
35
+ // src/modifiers.ts
36
+ function applyModifier(element, modifier, theme) {
37
+ if (!modifier) return;
38
+ const s = element.style;
39
+ if (modifier.alignment) {
40
+ switch (modifier.alignment) {
41
+ // Single-axis (Column / Row children)
42
+ case "center":
43
+ case "center_horizontally":
44
+ case "center_vertically":
45
+ s.alignSelf = "center";
46
+ break;
47
+ case "start":
48
+ case "top":
49
+ s.alignSelf = "flex-start";
50
+ break;
51
+ case "end":
52
+ case "bottom":
53
+ s.alignSelf = "flex-end";
54
+ break;
55
+ // Two-axis (Box children — grid layout)
56
+ case "top_start":
57
+ s.alignSelf = "start";
58
+ s.justifySelf = "start";
59
+ break;
60
+ case "top_center":
61
+ s.alignSelf = "start";
62
+ s.justifySelf = "center";
63
+ break;
64
+ case "top_end":
65
+ s.alignSelf = "start";
66
+ s.justifySelf = "end";
67
+ break;
68
+ case "center_start":
69
+ s.alignSelf = "center";
70
+ s.justifySelf = "start";
71
+ break;
72
+ case "center_end":
73
+ s.alignSelf = "center";
74
+ s.justifySelf = "end";
75
+ break;
76
+ case "bottom_start":
77
+ s.alignSelf = "end";
78
+ s.justifySelf = "start";
79
+ break;
80
+ case "bottom_center":
81
+ s.alignSelf = "end";
82
+ s.justifySelf = "center";
83
+ break;
84
+ case "bottom_end":
85
+ s.alignSelf = "end";
86
+ s.justifySelf = "end";
87
+ break;
88
+ }
89
+ }
90
+ if (modifier.weight != null) {
91
+ s.flex = `${modifier.weight}`;
92
+ s.minWidth = "0";
93
+ s.minHeight = "0";
94
+ }
95
+ if (modifier.action) {
96
+ const action = modifier.action;
97
+ s.cursor = "pointer";
98
+ element.addEventListener("click", (e) => {
99
+ e.stopPropagation();
100
+ element.dispatchEvent(new CustomEvent("sdui:action", {
101
+ bubbles: true,
102
+ detail: { type: action.type, params: action.params }
103
+ }));
104
+ });
105
+ }
106
+ const mod = modifier;
107
+ const hasAspectRatio = "aspectRatio" in mod && mod.aspectRatio != null;
108
+ const hasWidth = "width" in mod && mod.width;
109
+ const hasHeight = "height" in mod && mod.height;
110
+ if (hasWidth) {
111
+ const w = resolveSize(mod.width);
112
+ if (w) s.width = w;
113
+ }
114
+ if (hasHeight && !(hasAspectRatio && hasWidth)) {
115
+ const h = resolveSize(mod.height);
116
+ if (h) s.height = h;
117
+ }
118
+ if (hasAspectRatio) {
119
+ s.aspectRatio = `${mod.aspectRatio}`;
120
+ }
121
+ if ("padding" in mod && mod.padding) {
122
+ const p = resolveSpacing(mod.padding);
123
+ if (p.top) s.paddingTop = p.top;
124
+ if (p.bottom) s.paddingBottom = p.bottom;
125
+ if (p.left) s.paddingLeft = p.left;
126
+ if (p.right) s.paddingRight = p.right;
127
+ }
128
+ if ("margin" in mod && mod.margin) {
129
+ const m = resolveSpacing(mod.margin);
130
+ if (m.top) s.marginTop = m.top;
131
+ if (m.bottom) s.marginBottom = m.bottom;
132
+ if (m.left) s.marginLeft = m.left;
133
+ if (m.right) s.marginRight = m.right;
134
+ }
135
+ if ("backgroundColor" in mod && mod.backgroundColor) {
136
+ applyBackground(s, mod.backgroundColor, theme);
137
+ }
138
+ }
139
+ function applyBackground(s, bg, theme) {
140
+ if (!bg.colors || bg.colors.length === 0) return;
141
+ const colors = bg.colors.map((c) => resolveColor(c, theme)).filter(Boolean);
142
+ if (colors.length === 0) return;
143
+ switch (bg.type) {
144
+ case "single":
145
+ s.backgroundColor = colors[0];
146
+ break;
147
+ case "vertical_gradient":
148
+ s.background = `linear-gradient(to bottom, ${colors.join(", ")})`;
149
+ break;
150
+ case "horizontal_gradient":
151
+ s.background = `linear-gradient(to right, ${colors.join(", ")})`;
152
+ break;
153
+ case "linear_gradient":
154
+ s.background = `linear-gradient(${colors.join(", ")})`;
155
+ break;
156
+ default:
157
+ if (colors.length === 1) {
158
+ s.backgroundColor = colors[0];
159
+ }
160
+ break;
161
+ }
162
+ }
163
+
164
+ // src/components/text.ts
165
+ function renderText(component, theme) {
166
+ const props = component.properties;
167
+ const el = document.createElement("span");
168
+ el.textContent = props?.text ?? "";
169
+ if (props?.fontSize != null) {
170
+ el.style.fontSize = `${props.fontSize}px`;
171
+ }
172
+ if (props?.color) {
173
+ const c = resolveColor(props.color, theme);
174
+ if (c) el.style.color = c;
175
+ }
176
+ if (props?.fontWeight) {
177
+ switch (props.fontWeight) {
178
+ case "bold":
179
+ el.style.fontWeight = "700";
180
+ break;
181
+ case "semi_bold":
182
+ el.style.fontWeight = "600";
183
+ break;
184
+ case "medium":
185
+ el.style.fontWeight = "500";
186
+ break;
187
+ case "normal":
188
+ el.style.fontWeight = "400";
189
+ break;
190
+ }
191
+ }
192
+ if (props?.textAlign) {
193
+ el.style.display = "block";
194
+ switch (props.textAlign) {
195
+ case "left":
196
+ case "start":
197
+ el.style.textAlign = "left";
198
+ break;
199
+ case "right":
200
+ case "end":
201
+ el.style.textAlign = "right";
202
+ break;
203
+ case "center":
204
+ el.style.textAlign = "center";
205
+ break;
206
+ case "justify":
207
+ el.style.textAlign = "justify";
208
+ break;
209
+ }
210
+ }
211
+ if (props?.overflow === "ellipsis") {
212
+ el.style.textOverflow = "ellipsis";
213
+ el.style.overflow = "hidden";
214
+ el.style.whiteSpace = "nowrap";
215
+ }
216
+ if (props?.maxLines != null) {
217
+ el.style.display = "-webkit-box";
218
+ el.style.webkitLineClamp = `${props.maxLines}`;
219
+ el.style["-webkit-box-orient"] = "vertical";
220
+ el.style.overflow = "hidden";
221
+ }
222
+ if (props?.minLines != null) {
223
+ el.style.minHeight = `${props.minLines * (props.fontSize ?? 16) * 1.4}px`;
224
+ }
225
+ applyModifier(el, props?.modifier, theme);
226
+ return el;
227
+ }
228
+
229
+ // src/components/image.ts
230
+ function renderImage(component, theme) {
231
+ const props = component.properties;
232
+ const el = document.createElement("img");
233
+ el.style.display = "block";
234
+ el.style.boxSizing = "border-box";
235
+ if (props?.image) {
236
+ el.src = props.image;
237
+ }
238
+ if (props?.contentDescription) {
239
+ el.alt = props.contentDescription;
240
+ }
241
+ switch (props?.scaleType) {
242
+ case "crop":
243
+ el.style.objectFit = "cover";
244
+ break;
245
+ case "fill_bounds":
246
+ el.style.objectFit = "fill";
247
+ break;
248
+ case "fill_width":
249
+ el.style.objectFit = "cover";
250
+ el.style.width = "100%";
251
+ break;
252
+ case "fill_height":
253
+ el.style.objectFit = "cover";
254
+ el.style.height = "100%";
255
+ break;
256
+ case "none":
257
+ el.style.objectFit = "none";
258
+ break;
259
+ default:
260
+ el.style.objectFit = "contain";
261
+ break;
262
+ }
263
+ if (props?.tintColor) {
264
+ const c = resolveColor(props.tintColor, theme);
265
+ if (c) {
266
+ el.style.filter = `opacity(0.5) drop-shadow(0 0 0 ${c})`;
267
+ }
268
+ }
269
+ if (props?.aspectRatio != null) {
270
+ el.style.aspectRatio = `${props.aspectRatio}`;
271
+ }
272
+ applyModifier(el, props?.modifier, theme);
273
+ return el;
274
+ }
275
+
276
+ // src/components/box.ts
277
+ function renderBox(component, theme) {
278
+ const props = component.properties;
279
+ const el = document.createElement("div");
280
+ el.style.display = "grid";
281
+ el.style.position = "relative";
282
+ if (props?.alignment) {
283
+ switch (props.alignment) {
284
+ case "top_start":
285
+ el.style.justifyItems = "start";
286
+ el.style.alignItems = "start";
287
+ break;
288
+ case "top_center":
289
+ el.style.justifyItems = "center";
290
+ el.style.alignItems = "start";
291
+ break;
292
+ case "top_end":
293
+ el.style.justifyItems = "end";
294
+ el.style.alignItems = "start";
295
+ break;
296
+ case "center_start":
297
+ el.style.justifyItems = "start";
298
+ el.style.alignItems = "center";
299
+ break;
300
+ case "center":
301
+ el.style.justifyItems = "center";
302
+ el.style.alignItems = "center";
303
+ break;
304
+ case "center_end":
305
+ el.style.justifyItems = "end";
306
+ el.style.alignItems = "center";
307
+ break;
308
+ case "bottom_start":
309
+ el.style.justifyItems = "start";
310
+ el.style.alignItems = "end";
311
+ break;
312
+ case "bottom_center":
313
+ el.style.justifyItems = "center";
314
+ el.style.alignItems = "end";
315
+ break;
316
+ case "bottom_end":
317
+ el.style.justifyItems = "end";
318
+ el.style.alignItems = "end";
319
+ break;
320
+ }
321
+ }
322
+ applyModifier(el, props?.modifier, theme);
323
+ return el;
324
+ }
325
+
326
+ // src/components/column.ts
327
+ function renderColumn(component, theme) {
328
+ const props = component.properties;
329
+ const el = document.createElement("div");
330
+ el.style.display = "flex";
331
+ el.style.flexDirection = "column";
332
+ if (props?.verticalArrangement) {
333
+ const arr = props.verticalArrangement;
334
+ switch (arr.type) {
335
+ case "top":
336
+ el.style.justifyContent = "flex-start";
337
+ break;
338
+ case "center":
339
+ el.style.justifyContent = "center";
340
+ break;
341
+ case "bottom":
342
+ el.style.justifyContent = "flex-end";
343
+ break;
344
+ case "space_between":
345
+ el.style.justifyContent = "space-between";
346
+ break;
347
+ case "space_evenly":
348
+ el.style.justifyContent = "space-evenly";
349
+ break;
350
+ case "space_around":
351
+ el.style.justifyContent = "space-around";
352
+ break;
353
+ case "spaced_by":
354
+ el.style.justifyContent = "flex-start";
355
+ if (arr.spacing != null) {
356
+ el.style.gap = `${arr.spacing}px`;
357
+ }
358
+ break;
359
+ }
360
+ }
361
+ if (props?.horizontalAlignment) {
362
+ switch (props.horizontalAlignment) {
363
+ case "start":
364
+ el.style.alignItems = "flex-start";
365
+ break;
366
+ case "end":
367
+ el.style.alignItems = "flex-end";
368
+ break;
369
+ case "center_horizontally":
370
+ el.style.alignItems = "center";
371
+ break;
372
+ }
373
+ }
374
+ if (props?.canScroll) {
375
+ el.style.overflowY = "auto";
376
+ }
377
+ applyModifier(el, props?.modifier, theme);
378
+ return el;
379
+ }
380
+
381
+ // src/components/row.ts
382
+ function renderRow(component, theme) {
383
+ const props = component.properties;
384
+ const el = document.createElement("div");
385
+ el.style.display = "flex";
386
+ el.style.flexDirection = "row";
387
+ if (props?.horizontalArrangement) {
388
+ const arr = props.horizontalArrangement;
389
+ switch (arr.type) {
390
+ case "start":
391
+ el.style.justifyContent = "flex-start";
392
+ break;
393
+ case "center":
394
+ el.style.justifyContent = "center";
395
+ break;
396
+ case "end":
397
+ el.style.justifyContent = "flex-end";
398
+ break;
399
+ case "space_between":
400
+ el.style.justifyContent = "space-between";
401
+ break;
402
+ case "space_evenly":
403
+ el.style.justifyContent = "space-evenly";
404
+ break;
405
+ case "space_around":
406
+ el.style.justifyContent = "space-around";
407
+ break;
408
+ case "spaced_by":
409
+ el.style.justifyContent = "flex-start";
410
+ if (arr.spacing != null) {
411
+ el.style.gap = `${arr.spacing}px`;
412
+ }
413
+ break;
414
+ }
415
+ }
416
+ if (props?.verticalAlignment) {
417
+ switch (props.verticalAlignment) {
418
+ case "top":
419
+ el.style.alignItems = "flex-start";
420
+ break;
421
+ case "bottom":
422
+ el.style.alignItems = "flex-end";
423
+ break;
424
+ case "center_vertically":
425
+ el.style.alignItems = "center";
426
+ break;
427
+ }
428
+ }
429
+ if (props?.canScroll) {
430
+ el.style.overflowX = "auto";
431
+ }
432
+ applyModifier(el, props?.modifier, theme);
433
+ return el;
434
+ }
435
+
436
+ // src/components/lazy-column.ts
437
+ function renderLazyColumn(component, theme) {
438
+ const props = component.properties;
439
+ const el = document.createElement("div");
440
+ el.style.display = "flex";
441
+ el.style.flexDirection = "column";
442
+ el.style.overflowY = "auto";
443
+ if (props?.verticalArrangement) {
444
+ const arr = props.verticalArrangement;
445
+ switch (arr.type) {
446
+ case "top":
447
+ el.style.justifyContent = "flex-start";
448
+ break;
449
+ case "center":
450
+ el.style.justifyContent = "center";
451
+ break;
452
+ case "bottom":
453
+ el.style.justifyContent = "flex-end";
454
+ break;
455
+ case "space_between":
456
+ el.style.justifyContent = "space-between";
457
+ break;
458
+ case "space_evenly":
459
+ el.style.justifyContent = "space-evenly";
460
+ break;
461
+ case "space_around":
462
+ el.style.justifyContent = "space-around";
463
+ break;
464
+ case "spaced_by":
465
+ el.style.justifyContent = "flex-start";
466
+ if (arr.spacing != null) {
467
+ el.style.gap = `${arr.spacing}px`;
468
+ }
469
+ break;
470
+ }
471
+ }
472
+ applyModifier(el, props?.modifier, theme);
473
+ return el;
474
+ }
475
+
476
+ // src/components/index.ts
477
+ var renderers = {
478
+ text: renderText,
479
+ image: renderImage,
480
+ box: renderBox,
481
+ column: renderColumn,
482
+ row: renderRow,
483
+ lazy_column: renderLazyColumn
484
+ };
485
+ function getRenderer(type) {
486
+ return renderers[type];
487
+ }
488
+
489
+ // src/render.ts
490
+ function renderComponent(component, theme) {
491
+ const renderer = getRenderer(component.type);
492
+ if (!renderer) {
493
+ console.warn(`[sdui] Unknown component type: "${component.type}"`);
494
+ const el2 = document.createElement("div");
495
+ el2.dataset.sduiUnknown = component.type;
496
+ return el2;
497
+ }
498
+ const el = renderer(component, theme);
499
+ el.dataset.sduiType = component.type;
500
+ if (component.__builderId) {
501
+ el.dataset.sduiId = component.__builderId;
502
+ }
503
+ if (component.children && component.children.length > 0) {
504
+ for (const child of component.children) {
505
+ el.appendChild(renderComponent(child, theme));
506
+ }
507
+ }
508
+ return el;
509
+ }
510
+
511
+ // src/component-meta.ts
512
+ function sizeModelProperties() {
513
+ return [
514
+ { name: "value", label: "Value", type: "number" },
515
+ { name: "unit", label: "Unit", type: "enum", enumValues: ["dp", "match_parent", "wrap_content"] }
516
+ ];
517
+ }
518
+ function spacingModelProperties() {
519
+ return [
520
+ { name: "top", label: "Top", type: "number" },
521
+ { name: "bottom", label: "Bottom", type: "number" },
522
+ { name: "start", label: "Start", type: "number" },
523
+ { name: "end", label: "End", type: "number" }
524
+ ];
525
+ }
526
+ function colorModelProperties() {
527
+ return [
528
+ { name: "light", label: "Light", type: "color" },
529
+ { name: "dark", label: "Dark", type: "color" }
530
+ ];
531
+ }
532
+ function actionModelProperties() {
533
+ return [
534
+ { name: "type", label: "Action Type", type: "string" },
535
+ { name: "params", label: "Parameters", type: "object" }
536
+ ];
537
+ }
538
+ function backgroundColorModelProperties() {
539
+ return [
540
+ {
541
+ name: "colors",
542
+ label: "Colors",
543
+ type: "object",
544
+ isArray: true,
545
+ properties: colorModelProperties()
546
+ },
547
+ {
548
+ name: "type",
549
+ label: "Background Type",
550
+ type: "enum",
551
+ enumValues: ["single", "vertical_gradient", "horizontal_gradient", "linear_gradient"]
552
+ }
553
+ ];
554
+ }
555
+ function modifierBaseProperties() {
556
+ return [
557
+ {
558
+ name: "alignment",
559
+ label: "Alignment",
560
+ type: "enum",
561
+ enumValues: ["start", "end", "center", "center_horizontally", "center_vertically", "top", "bottom", "top_start", "top_center", "top_end", "center_start", "center_end", "bottom_start", "bottom_center", "bottom_end"]
562
+ },
563
+ { name: "weight", label: "Weight", type: "number" },
564
+ { name: "action", label: "Action", type: "object", properties: actionModelProperties() }
565
+ ];
566
+ }
567
+ function textModifierDescriptor() {
568
+ return {
569
+ name: "modifier",
570
+ label: "Modifier",
571
+ type: "object",
572
+ properties: [
573
+ ...modifierBaseProperties(),
574
+ { name: "width", label: "Width", type: "object", properties: sizeModelProperties() },
575
+ { name: "margin", label: "Margin", type: "object", properties: spacingModelProperties() }
576
+ ]
577
+ };
578
+ }
579
+ function imageModifierDescriptor() {
580
+ return {
581
+ name: "modifier",
582
+ label: "Modifier",
583
+ type: "object",
584
+ properties: [
585
+ ...modifierBaseProperties(),
586
+ { name: "width", label: "Width", type: "object", properties: sizeModelProperties() },
587
+ { name: "height", label: "Height", type: "object", properties: sizeModelProperties() },
588
+ { name: "padding", label: "Padding", type: "object", properties: spacingModelProperties() },
589
+ { name: "margin", label: "Margin", type: "object", properties: spacingModelProperties() }
590
+ ]
591
+ };
592
+ }
593
+ function containerModifierDescriptor() {
594
+ return {
595
+ name: "modifier",
596
+ label: "Modifier",
597
+ type: "object",
598
+ properties: [
599
+ ...modifierBaseProperties(),
600
+ { name: "backgroundColor", label: "Background Color", type: "object", properties: backgroundColorModelProperties() },
601
+ { name: "width", label: "Width", type: "object", properties: sizeModelProperties() },
602
+ { name: "height", label: "Height", type: "object", properties: sizeModelProperties() },
603
+ { name: "padding", label: "Padding", type: "object", properties: spacingModelProperties() },
604
+ { name: "margin", label: "Margin", type: "object", properties: spacingModelProperties() },
605
+ { name: "aspectRatio", label: "Aspect Ratio", type: "number" }
606
+ ]
607
+ };
608
+ }
609
+ function verticalArrangementDescriptor() {
610
+ return {
611
+ name: "verticalArrangement",
612
+ label: "Vertical Arrangement",
613
+ type: "object",
614
+ properties: [
615
+ {
616
+ name: "type",
617
+ label: "Type",
618
+ type: "enum",
619
+ enumValues: ["top", "center", "bottom", "space_evenly", "space_between", "space_around", "spaced_by"]
620
+ },
621
+ { name: "spacing", label: "Spacing", type: "number" }
622
+ ]
623
+ };
624
+ }
625
+ function horizontalArrangementDescriptor() {
626
+ return {
627
+ name: "horizontalArrangement",
628
+ label: "Horizontal Arrangement",
629
+ type: "object",
630
+ properties: [
631
+ {
632
+ name: "type",
633
+ label: "Type",
634
+ type: "enum",
635
+ enumValues: ["start", "center", "end", "space_between", "space_evenly", "space_around", "spaced_by"]
636
+ },
637
+ { name: "spacing", label: "Spacing", type: "number" }
638
+ ]
639
+ };
640
+ }
641
+ var COMPONENT_DEFINITIONS = [
642
+ {
643
+ type: "text",
644
+ label: "Text",
645
+ acceptsChildren: false,
646
+ properties: [
647
+ { name: "text", label: "Text", type: "string", required: true, defaultValue: "Text" },
648
+ { name: "fontSize", label: "Font Size", type: "number" },
649
+ { name: "color", label: "Color", type: "object", properties: colorModelProperties() },
650
+ { name: "fontWeight", label: "Font Weight", type: "enum", enumValues: ["bold", "medium", "normal", "semi_bold"] },
651
+ { name: "textAlign", label: "Text Align", type: "enum", enumValues: ["left", "right", "center", "justify", "start", "end", "unspecified"] },
652
+ { name: "overflow", label: "Overflow", type: "enum", enumValues: ["none", "ellipsis"] },
653
+ { name: "maxLines", label: "Max Lines", type: "number" },
654
+ { name: "minLines", label: "Min Lines", type: "number", defaultValue: 1 },
655
+ textModifierDescriptor()
656
+ ]
657
+ },
658
+ {
659
+ type: "image",
660
+ label: "Image",
661
+ acceptsChildren: false,
662
+ properties: [
663
+ { name: "image", label: "Image URL", type: "string", required: true },
664
+ { name: "scaleType", label: "Scale Type", type: "enum", enumValues: ["fit", "crop", "fill_height", "fill_width", "fill_bounds", "inside", "none"] },
665
+ { name: "contentDescription", label: "Content Description", type: "string" },
666
+ { name: "aspectRatio", label: "Aspect Ratio", type: "number" },
667
+ { name: "tintColor", label: "Tint Color", type: "object", properties: colorModelProperties() },
668
+ imageModifierDescriptor()
669
+ ]
670
+ },
671
+ {
672
+ type: "box",
673
+ label: "Box",
674
+ acceptsChildren: true,
675
+ properties: [
676
+ containerModifierDescriptor(),
677
+ {
678
+ name: "alignment",
679
+ label: "Alignment",
680
+ type: "enum",
681
+ enumValues: ["top_start", "top_center", "top_end", "center_start", "center", "center_end", "bottom_start", "bottom_center", "bottom_end"]
682
+ }
683
+ ]
684
+ },
685
+ {
686
+ type: "column",
687
+ label: "Column",
688
+ acceptsChildren: true,
689
+ properties: [
690
+ containerModifierDescriptor(),
691
+ verticalArrangementDescriptor(),
692
+ { name: "horizontalAlignment", label: "Horizontal Alignment", type: "enum", enumValues: ["start", "end", "center_horizontally"] },
693
+ { name: "canScroll", label: "Can Scroll", type: "boolean" }
694
+ ]
695
+ },
696
+ {
697
+ type: "row",
698
+ label: "Row",
699
+ acceptsChildren: true,
700
+ properties: [
701
+ containerModifierDescriptor(),
702
+ { name: "verticalAlignment", label: "Vertical Alignment", type: "enum", enumValues: ["top", "bottom", "center_vertically"] },
703
+ horizontalArrangementDescriptor(),
704
+ { name: "canScroll", label: "Can Scroll", type: "boolean" }
705
+ ]
706
+ },
707
+ {
708
+ type: "lazy_column",
709
+ label: "Lazy Column",
710
+ acceptsChildren: true,
711
+ properties: [
712
+ containerModifierDescriptor(),
713
+ verticalArrangementDescriptor()
714
+ ]
715
+ }
716
+ ];
717
+
718
+ // src/index.ts
719
+ function render(json, container, options) {
720
+ const screen = JSON.parse(json);
721
+ const theme = options?.theme ?? "light";
722
+ const root = renderComponent(screen.root, theme);
723
+ container.innerHTML = "";
724
+ container.appendChild(root);
725
+ }
726
+
727
+ // src/react/SduiRenderer.tsx
728
+ import { jsx, jsxs } from "react/jsx-runtime";
729
+ function SduiRenderer({
730
+ json,
731
+ theme = "light",
732
+ deviceWidth,
733
+ deviceHeight,
734
+ filePreviewMap,
735
+ selectedId,
736
+ onClickNode,
737
+ onClickOutside,
738
+ className,
739
+ style
740
+ }) {
741
+ const renderRef = useRef(null);
742
+ const wrapperRef = useRef(null);
743
+ const [containerSize, setContainerSize] = useState({ w: 0, h: 0 });
744
+ const [error, setError] = useState(null);
745
+ useEffect(() => {
746
+ const el = wrapperRef.current;
747
+ if (!el) return;
748
+ const ro = new ResizeObserver((entries) => {
749
+ const { width, height } = entries[0].contentRect;
750
+ setContainerSize({ w: width, h: height });
751
+ });
752
+ ro.observe(el);
753
+ return () => ro.disconnect();
754
+ }, []);
755
+ useEffect(() => {
756
+ const el = renderRef.current;
757
+ if (!el) return;
758
+ try {
759
+ let renderJson = json;
760
+ if (filePreviewMap) {
761
+ for (const [fileName, blobUrl] of Object.entries(filePreviewMap)) {
762
+ renderJson = renderJson.replaceAll(JSON.stringify(fileName), JSON.stringify(blobUrl));
763
+ }
764
+ }
765
+ JSON.parse(renderJson);
766
+ setError(null);
767
+ render(renderJson, el, { theme });
768
+ } catch (err) {
769
+ setError(err instanceof Error ? err.message : "Render error");
770
+ el.innerHTML = "";
771
+ }
772
+ }, [json, theme, filePreviewMap]);
773
+ useEffect(() => {
774
+ const el = renderRef.current;
775
+ if (!el) return;
776
+ el.querySelectorAll("[data-sdui-selected]").forEach((node) => {
777
+ node.removeAttribute("data-sdui-selected");
778
+ });
779
+ if (selectedId) {
780
+ const selected = el.querySelector(`[data-sdui-id="${selectedId}"]`);
781
+ if (selected) {
782
+ selected.setAttribute("data-sdui-selected", "true");
783
+ }
784
+ }
785
+ }, [selectedId, json]);
786
+ useEffect(() => {
787
+ const el = renderRef.current;
788
+ if (!el || !onClickNode) return;
789
+ function handleClick(e) {
790
+ e.stopPropagation();
791
+ const target = e.target.closest("[data-sdui-id]");
792
+ if (target) {
793
+ onClickNode(target.dataset.sduiId);
794
+ }
795
+ }
796
+ el.addEventListener("click", handleClick);
797
+ return () => el.removeEventListener("click", handleClick);
798
+ }, [onClickNode]);
799
+ const padding = 48;
800
+ const totalH = deviceHeight + 10;
801
+ const scaleW = containerSize.w > 0 && deviceWidth > containerSize.w - padding ? (containerSize.w - padding) / deviceWidth : 1;
802
+ const scaleH = containerSize.h > 0 && totalH > containerSize.h - padding ? (containerSize.h - padding) / totalH : 1;
803
+ const scale = Math.min(scaleW, scaleH, 1);
804
+ let rootScrollable = false;
805
+ try {
806
+ const parsed = JSON.parse(json);
807
+ const root = parsed.root;
808
+ if (root) {
809
+ rootScrollable = root.type === "lazy_column" || root.properties?.canScroll === true;
810
+ }
811
+ } catch {
812
+ }
813
+ return /* @__PURE__ */ jsxs(
814
+ "div",
815
+ {
816
+ ref: wrapperRef,
817
+ className,
818
+ style: {
819
+ flex: 1,
820
+ overflow: "hidden",
821
+ display: "flex",
822
+ justifyContent: "center",
823
+ alignItems: "flex-start",
824
+ padding: "24px 16px",
825
+ ...style
826
+ },
827
+ onClick: onClickOutside,
828
+ children: [
829
+ /* @__PURE__ */ jsxs(
830
+ "div",
831
+ {
832
+ onClick: (e) => e.stopPropagation(),
833
+ style: {
834
+ position: "relative",
835
+ alignSelf: "flex-start",
836
+ width: `${deviceWidth}px`,
837
+ transform: scale < 1 ? `scale(${scale})` : void 0,
838
+ transformOrigin: "top center"
839
+ },
840
+ children: [
841
+ /* @__PURE__ */ jsx(
842
+ "div",
843
+ {
844
+ style: {
845
+ borderRadius: "36px",
846
+ padding: "3px",
847
+ background: "linear-gradient(145deg, rgba(255,255,255,0.12), rgba(255,255,255,0.04))",
848
+ boxShadow: "0 24px 80px rgba(0,0,0,0.5), 0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06)"
849
+ },
850
+ children: /* @__PURE__ */ jsxs("div", { style: { borderRadius: "33px", overflow: "hidden", background: "#1c1c1e" }, children: [
851
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "8px 24px", height: "28px" }, children: [
852
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", fontWeight: 600, color: "rgba(255,255,255,0.7)", fontFamily: "system-ui" }, children: "9:41" }),
853
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "6px" }, children: [
854
+ /* @__PURE__ */ jsxs("svg", { width: "14", height: "10", viewBox: "0 0 14 10", fill: "none", children: [
855
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "6", width: "2.5", height: "4", rx: "0.5", fill: "rgba(255,255,255,0.5)" }),
856
+ /* @__PURE__ */ jsx("rect", { x: "3.8", y: "4", width: "2.5", height: "6", rx: "0.5", fill: "rgba(255,255,255,0.5)" }),
857
+ /* @__PURE__ */ jsx("rect", { x: "7.6", y: "2", width: "2.5", height: "8", rx: "0.5", fill: "rgba(255,255,255,0.5)" }),
858
+ /* @__PURE__ */ jsx("rect", { x: "11.4", y: "0", width: "2.5", height: "10", rx: "0.5", fill: "rgba(255,255,255,0.5)" })
859
+ ] }),
860
+ /* @__PURE__ */ jsxs("svg", { width: "20", height: "10", viewBox: "0 0 20 10", fill: "none", children: [
861
+ /* @__PURE__ */ jsx("rect", { x: "0.5", y: "0.5", width: "16", height: "9", rx: "2", stroke: "rgba(255,255,255,0.4)", strokeWidth: "1" }),
862
+ /* @__PURE__ */ jsx("rect", { x: "17", y: "3", width: "2", height: "4", rx: "0.5", fill: "rgba(255,255,255,0.3)" }),
863
+ /* @__PURE__ */ jsx("rect", { x: "1.5", y: "1.5", width: "14", height: "7", rx: "1", fill: "rgba(255,255,255,0.5)" })
864
+ ] })
865
+ ] })
866
+ ] }),
867
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "4px" }, children: /* @__PURE__ */ jsx("div", { style: { width: "120px", height: "28px", background: "#000", borderRadius: "14px" } }) }),
868
+ /* @__PURE__ */ jsx(
869
+ "div",
870
+ {
871
+ ref: renderRef,
872
+ style: {
873
+ background: theme === "dark" ? "#121212" : "#ffffff",
874
+ color: theme === "dark" ? "#ffffff" : "#000000",
875
+ height: `${deviceHeight - 60}px`,
876
+ borderBottomLeftRadius: "33px",
877
+ borderBottomRightRadius: "33px",
878
+ overflow: rootScrollable ? "auto" : "hidden"
879
+ }
880
+ }
881
+ )
882
+ ] })
883
+ }
884
+ ),
885
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginTop: "-18px", position: "relative", zIndex: 10 }, children: /* @__PURE__ */ jsx("div", { style: { width: "120px", height: "4px", background: "rgba(255,255,255,0.2)", borderRadius: "2px" } }) })
886
+ ]
887
+ }
888
+ ),
889
+ error && /* @__PURE__ */ jsx("div", { style: {
890
+ position: "absolute",
891
+ bottom: "24px",
892
+ left: "50%",
893
+ transform: "translateX(-50%)",
894
+ width: "100%",
895
+ maxWidth: "375px",
896
+ padding: "12px",
897
+ borderRadius: "8px",
898
+ background: "rgba(239,68,68,0.1)",
899
+ border: "1px solid rgba(239,68,68,0.2)"
900
+ }, children: /* @__PURE__ */ jsx("p", { style: { fontSize: "13px", fontFamily: "monospace", color: "#ef4444", wordBreak: "break-all" }, children: error }) })
901
+ ]
902
+ }
903
+ );
904
+ }
905
+ export {
906
+ SduiRenderer
907
+ };
908
+ //# sourceMappingURL=index.js.map