tauri-kargo-tools 0.1.3 → 0.1.5

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,136 @@
1
+ // ./builder/boot-vue.ts
2
+ import {
3
+ Builder,
4
+ Ctx,
5
+ applyIdAndClass,
6
+ applySize,
7
+ bindVisible,
8
+ bindEnabled,
9
+ } from "../vue-builder";
10
+ import {
11
+ BootVueNode,
12
+ StaticBootVueNode,
13
+ Vue,
14
+ } from "../vue-model";
15
+
16
+ /**
17
+ * BootVue — bouton avec label dynamique (KeysOfType<T,string>) qui,
18
+ * au clic, exécute `action()` puis monte la Vue de `name` dans un conteneur local.
19
+ */
20
+ export function buildBootVue<T extends object>(
21
+ b: Builder,
22
+ node: BootVueNode<T, any, any, any>,
23
+ ctx: Ctx<T>
24
+ ) {
25
+ // wrapper racine du node
26
+
27
+
28
+
29
+ // bouton + conteneur d'accueil de la sous-UI
30
+ const btn = document.createElement("button");
31
+ applyIdAndClass(btn, node);
32
+
33
+
34
+ // taille appliquée au bouton (cohérent avec ButtonNode)
35
+ applySize(btn, node.width, node.height);
36
+
37
+ // visible sur le wrapper, enable sur le bouton
38
+ bindVisible(node as any, btn, ctx);
39
+ bindEnabled(node as any, btn, ctx);
40
+
41
+ // rendu du label dynamique
42
+ const labelKey = node.label as keyof T;
43
+ const render = (val: any) => b.renderTrigger(btn, String(val ?? ""), node.type);
44
+ render((ctx.obj as any)[labelKey]);
45
+
46
+ // écoute des changements du label
47
+ const offLabel = ctx.listener.listen(labelKey, (v: any) => render(v));
48
+ ctx.dataUnsubs.push(offLabel);
49
+
50
+ // logique de boot (montage/cleanup de la sous-UI)
51
+ let subStop: (() => void) | undefined;
52
+
53
+ const onClick = () => {
54
+ // 1) action()
55
+ try {
56
+ const fn = (ctx.obj as any)[node.factory];
57
+ if (typeof fn !== "function") return;
58
+
59
+ const child = fn.call(ctx.obj);
60
+
61
+ if (!child) return;
62
+ for (const u of ctx.domUnsubs) { try { u(); } catch { } }
63
+ for (const u of ctx.dataUnsubs) { try { u(); } catch { } }
64
+ b.bootInContainer(child)
65
+
66
+ // cleanup précédent
67
+ try { subStop?.(); } catch { }
68
+ } catch (e) {
69
+ console.warn("[bootVue.action] failed:", e);
70
+ }
71
+ };
72
+
73
+ btn.addEventListener("click", onClick);
74
+ ctx.domUnsubs.push(() => btn.removeEventListener("click", onClick));
75
+ ctx.domUnsubs.push(() => { try { subStop?.(); } catch { } });
76
+
77
+
78
+ ctx.add(btn);
79
+ }
80
+
81
+ /**
82
+ * StaticBootVue — bouton avec label fixe (string) qui,
83
+ * au clic, exécute `action()` puis monte la Vue de `name` dans un conteneur local.
84
+ */
85
+ export function buildStaticBootVue<T extends object>(
86
+ b: Builder,
87
+ node: StaticBootVueNode<T, any, any>,
88
+ ctx: Ctx<T>
89
+ ) {
90
+ // wrapper racine du node
91
+
92
+
93
+
94
+ // bouton + conteneur d'accueil de la sous-UI
95
+ const btn = document.createElement("button");
96
+ applyIdAndClass(btn, node);
97
+
98
+ // taille appliquée au bouton
99
+ applySize(btn, node.width, node.height);
100
+
101
+ // visible sur le wrapper, enable sur le bouton
102
+ bindVisible(node as any, btn, ctx);
103
+ bindEnabled(node as any, btn, ctx);
104
+
105
+ // rendu du label fixe (texte / html / img)
106
+ b.renderTrigger(btn, node.label, node.type);
107
+
108
+ // logique de boot (montage/cleanup de la sous-UI)
109
+ let subStop: (() => void) | undefined;
110
+
111
+ const onClick = () => {
112
+ // 1) action()
113
+ try {
114
+ const fn = (ctx.obj as any)[node.factory];
115
+ if (typeof fn !== "function") return;
116
+ const child = fn.call(ctx.obj);
117
+ if (!child) return;
118
+ for (const u of ctx.domUnsubs) { try { u(); } catch { } }
119
+ for (const u of ctx.dataUnsubs) { try { u(); } catch { } }
120
+ b.bootInContainer(child)
121
+
122
+ // cleanup précédent
123
+ try { subStop?.(); } catch { }
124
+ } catch (e) {
125
+ console.warn("[staticBootVue.action] failed:", e);
126
+ }
127
+ };
128
+
129
+ btn.addEventListener("click", onClick);
130
+ ctx.domUnsubs.push(() => btn.removeEventListener("click", onClick));
131
+ ctx.domUnsubs.push(() => { try { subStop?.(); } catch { } });
132
+
133
+
134
+
135
+ ctx.add(btn);
136
+ }
@@ -0,0 +1,121 @@
1
+ import { applyIdAndClass, applySize, bindVisibleEnabled, Builder, Ctx } from "../vue-builder";
2
+ import { ButtonNode, StaticButtonNode } from "../vue-model";
3
+
4
+ /* ----------- Button ----------- */
5
+ export function buildStaticButton<T extends object>(builder: Builder, node: StaticButtonNode<T>, ctx: Ctx<T>) {
6
+ const btn = document.createElement('button');
7
+ applyIdAndClass(btn, node);
8
+ btn.type = 'button';
9
+ applySize(btn, node.width, node.height);
10
+ ctx.add(btn);
11
+
12
+ // --- rendu du contenu (texte / html / img) ---
13
+ const ctype = (node as any).type as ('img' | 'html' | undefined);
14
+ const nameKey = (node as any).name as (keyof T | undefined);
15
+
16
+ const renderContent = (valFromModel?: any) => {
17
+ const fallback = node.label ?? '';
18
+ const value = (valFromModel !== undefined) ? String(valFromModel ?? '') :
19
+ (nameKey ? String((ctx.obj as any)[nameKey] ?? '') : String(fallback));
20
+
21
+ if (!ctype) {
22
+ if (btn.innerHTML !== '') btn.innerHTML = '';
23
+ if (btn.textContent !== fallback) btn.textContent = fallback;
24
+ return;
25
+ }
26
+
27
+ if (ctype === 'html') {
28
+ if (btn.innerHTML !== value) btn.innerHTML = value;
29
+ return;
30
+ }
31
+
32
+ // ctype === 'img'
33
+ btn.innerHTML = '';
34
+ const img = document.createElement('img');
35
+ if (value) img.setAttribute('src', value);
36
+ img.alt = typeof node.label === 'string' ? node.label : '';
37
+ img.style.maxWidth = '100%';
38
+ img.style.maxHeight = '100%';
39
+ img.style.pointerEvents = 'none';
40
+ btn.appendChild(img);
41
+ };
42
+
43
+ renderContent();
44
+
45
+ if (ctype && nameKey) {
46
+ const off = ctx.listener.listen(nameKey, (v) => renderContent(v));
47
+ ctx.dataUnsubs.push(off);
48
+ }
49
+
50
+ bindVisibleEnabled(node, btn, ctx);
51
+
52
+ const onClick = () => {
53
+ if (node.muted && (ctx.listener as any).withAllMuted) {
54
+ (ctx.listener as any).withAllMuted(() => { (ctx.obj as any)[node.action]!(); });
55
+ } else {
56
+ (ctx.obj as any)[node.action]!();
57
+ }
58
+ };
59
+ btn.addEventListener('click', onClick);
60
+ ctx.domUnsubs.push(() => btn.removeEventListener('click', onClick));
61
+ }
62
+
63
+ /* ----------- ButtonLabel ----------- */
64
+ export function buildButton<T extends object>(builder: Builder, node: ButtonNode<T, any>, ctx: Ctx<T>) {
65
+ const btn = document.createElement('button');
66
+ applyIdAndClass(btn, node);
67
+ btn.type = 'button';
68
+ applySize(btn, node.width, node.height);
69
+ ctx.add(btn);
70
+
71
+ const ctype = (node as any).type as ('img' | 'html' | undefined);
72
+ const sourceKey = ((node as any).name as (keyof T | undefined)) ?? (node.label as keyof T);
73
+
74
+ const renderFrom = (val: any) => {
75
+ const value = String(val ?? '');
76
+ if (!ctype) {
77
+ if (btn.innerHTML !== '') btn.innerHTML = '';
78
+ if (btn.textContent !== value) btn.textContent = value;
79
+ return;
80
+ }
81
+ if (ctype === 'html') {
82
+ if (btn.innerHTML !== value) btn.innerHTML = value;
83
+ return;
84
+ }
85
+ // ctype === 'img'
86
+ btn.innerHTML = '';
87
+ const img = document.createElement('img');
88
+ if (value) img.setAttribute('src', value);
89
+ const altText = String((ctx.obj as any)[node.label] ?? '');
90
+ img.alt = altText;
91
+ img.style.maxWidth = '100%';
92
+ img.style.maxHeight = '100%';
93
+ img.style.pointerEvents = 'none';
94
+ btn.appendChild(img);
95
+ };
96
+
97
+ renderFrom((ctx.obj as any)[sourceKey]);
98
+
99
+ const offSource = ctx.listener.listen(sourceKey, (v) => renderFrom(v));
100
+ ctx.dataUnsubs.push(offSource);
101
+
102
+ if (ctype === 'img' && sourceKey !== (node.label as keyof T)) {
103
+ const offAlt = ctx.listener.listen(node.label as keyof T, () => {
104
+ const img = btn.querySelector('img');
105
+ if (img) img.alt = String((ctx.obj as any)[node.label] ?? '');
106
+ });
107
+ ctx.dataUnsubs.push(offAlt);
108
+ }
109
+
110
+ bindVisibleEnabled(node, btn, ctx);
111
+
112
+ const onClick = () => {
113
+ if (node.muted && (ctx.listener as any).withAllMuted) {
114
+ (ctx.listener as any).withAllMuted(() => { (ctx.obj as any)[node.action]!(); });
115
+ } else {
116
+ (ctx.obj as any)[node.action]!();
117
+ }
118
+ };
119
+ btn.addEventListener('click', onClick);
120
+ ctx.domUnsubs.push(() => btn.removeEventListener('click', onClick));
121
+ }
@@ -0,0 +1,30 @@
1
+ import { applyIdAndClass, applySize, bindVisibleEnabled, Builder, Ctx } from "../vue-builder";
2
+ import { CustomNode } from "../vue-model";
3
+
4
+ /* ----------- Custom ----------- */
5
+ export function buildCustom<T extends object>(builder:Builder,node: CustomNode<T, any, any>, ctx: Ctx<T>) {
6
+ let el: HTMLElement | null = null;
7
+ try {
8
+ el = (ctx.obj as any)[node.factory]!();
9
+ } catch (e) {
10
+ console.warn('[custom] factory call failed:', e);
11
+ return;
12
+ }
13
+ if (!(el instanceof HTMLElement)) {
14
+ console.warn('[custom] factory did not return an HTMLElement');
15
+ return;
16
+ }
17
+
18
+ applyIdAndClass(el, node);
19
+ applySize(el, node.width, node.height);
20
+ ctx.add(el);
21
+
22
+ bindVisibleEnabled(node, el, ctx);
23
+
24
+ if (node.init) {
25
+ ctx.postInits.push(() => {
26
+ try { (ctx.obj as any)[node.init]!(); }
27
+ catch (e) { console.warn('[custom.init] call failed:', e); }
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,78 @@
1
+ import { applyIdAndClass, applySize, bindVisibleEnabled, Builder, Ctx, VueRuntime } from "../vue-builder";
2
+ import { DialogNode, Vue } from "../vue-model";
3
+
4
+ /* ----------- Dialog ----------- */
5
+ export function buildDialog<T extends object>(builder:Builder,node: DialogNode<T>, ctx: Ctx<T>) {
6
+ const btn = document.createElement('button');
7
+ btn.type = 'button';
8
+ applySize(btn, node.buttonWidth, node.buttonHeight);
9
+ // Rendu du trigger (texte/html/img) – source = `label`
10
+ builder.renderTrigger(btn, node.label, (node as any).type as ('html' | 'img' | undefined));
11
+ ctx.add(btn);
12
+
13
+ const dlg = document.createElement('dialog') as HTMLDialogElement;
14
+ applyIdAndClass(dlg, node);
15
+ applySize(dlg, node.width, node.height);
16
+ const host = document.createElement('div');
17
+ host.style.minWidth = '100%';
18
+ dlg.appendChild(host);
19
+
20
+ let child: VueRuntime<any> | null = null;
21
+ const clearChild = () => {
22
+ if (child) { try { child.stop(); } catch { } child = null; }
23
+ while (host.firstChild) host.removeChild(host.firstChild);
24
+ };
25
+
26
+ const mountFor = (value: any) => {
27
+ clearChild();
28
+ const ui = builder.findVueFor(value);
29
+ if (!ui) return false;
30
+ const wrap = document.createElement('div');
31
+ host.appendChild(wrap);
32
+ child = builder.bootInto(ui as Vue<any>, value, wrap);
33
+ return true;
34
+ };
35
+
36
+ const open = () => {
37
+ if (node.action) {
38
+ try { (ctx.obj as any)[node.action]!(); } catch { }
39
+ }
40
+ const value = (ctx.obj as any)[node.name];
41
+ if (value == null) {
42
+ try { dlg.close(); } catch { }
43
+ clearChild();
44
+ return;
45
+ }
46
+ const ok = mountFor(value);
47
+ if (!ok) return;
48
+ const modal = node.modal ?? true;
49
+ if (modal && 'showModal' in dlg) dlg.showModal(); else dlg.show();
50
+ };
51
+
52
+ const close = () => {
53
+ try { dlg.close(); } catch { }
54
+ clearChild();
55
+ };
56
+
57
+ const offField = ctx.listener.listen(node.name as keyof T, (v) => {
58
+ if (v == null) close();
59
+ else if (dlg.open) mountFor(v);
60
+ });
61
+ ctx.dataUnsubs.push(offField);
62
+
63
+ bindVisibleEnabled(node, btn, ctx);
64
+
65
+ if (node.closeOnBackdrop) {
66
+ dlg.addEventListener('click', (e) => { if (e.target === dlg) close(); });
67
+ }
68
+ if (!(node.closeOnEsc ?? false)) {
69
+ dlg.addEventListener('cancel', (e) => e.preventDefault());
70
+ }
71
+ dlg.addEventListener('close', () => clearChild());
72
+
73
+ btn.addEventListener('click', open);
74
+ ctx.domUnsubs.push(() => btn.removeEventListener('click', open));
75
+
76
+ ctx.add(dlg);
77
+ ctx.domUnsubs.push(() => { try { dlg.close(); } catch { } });
78
+ }
@@ -0,0 +1,34 @@
1
+ import { applyIdAndClass, applySize, Builder, Ctx } from "../vue-builder";
2
+ import { FlowNode } from "../vue-model";
3
+
4
+ /* ----------- Flow ----------- */
5
+ export function buildFlow<T extends object>(builder:Builder,node: FlowNode<T>, ctx: Ctx<T>) {
6
+ const div = document.createElement('div');
7
+ applyIdAndClass(div, node);
8
+ div.style.display = 'flex';
9
+ div.style.flexDirection = node.orientation === 'row' ? 'row' : 'column';
10
+ applySize(div, node.width, node.height);
11
+
12
+ if (node.gap !== undefined) (div.style as any).gap = typeof node.gap === 'number' ? `${node.gap}px` : String(node.gap);
13
+ const mapJustify: Record<string, string> = {
14
+ start: 'flex-start', end: 'flex-end', center: 'center',
15
+ 'space-between': 'space-between', 'space-around': 'space-around', 'space-evenly': 'space-evenly'
16
+ };
17
+ const mapAlign: Record<string, string> = {
18
+ start: 'flex-start', end: 'flex-end', center: 'center', stretch: 'stretch'
19
+ };
20
+ if (node.justify) div.style.justifyContent = mapJustify[node.justify];
21
+ if (node.align) div.style.alignItems = mapAlign[node.align];
22
+ if (node.wrap) div.style.flexWrap = 'wrap';
23
+ if (node.style) for (const k of Object.keys(node.style) as Array<keyof CSSStyleDeclaration>) {
24
+ const v = node.style[k]; if (v != null) (div.style as any)[k] = v as any;
25
+ }
26
+ if (node.panel) div.classList.add('panel');
27
+
28
+ const childEls: HTMLElement[] = [];
29
+ const childCtx: Ctx<T> = { ...ctx, add: (el) => childEls.push(el) };
30
+ builder.buildNodes(node.children, childCtx);
31
+ for (const n of childEls) div.appendChild(n);
32
+
33
+ ctx.add(div);
34
+ }
@@ -0,0 +1,27 @@
1
+ import { applyIdAndClass, applySize, bindVisibleEnabled, Builder, Ctx } from "../vue-builder";
2
+ import { ImgNode } from "../vue-model";
3
+
4
+
5
+ /* ----------- Img ----------- */
6
+ export function buildImg<T extends object>(builder:Builder,node: ImgNode<T, any>, ctx: Ctx<T>) {
7
+ const img = document.createElement('img');
8
+ applyIdAndClass(img, node);
9
+ applySize(img, node.width, node.height);
10
+ if (node.alt != null) img.alt = node.alt;
11
+ const initial = String((ctx.obj as any)[node.url] ?? '');
12
+ if (initial !== '') img.setAttribute('src', initial);
13
+
14
+ ctx.add(img);
15
+
16
+ bindVisibleEnabled(node, img, ctx);
17
+
18
+ const offUrl = ctx.listener.listen(node.url as keyof T, (v) => {
19
+ const s = String(v ?? '');
20
+ const cur = img.getAttribute('src') ?? '';
21
+ if (cur !== s) {
22
+ if (s === '') img.removeAttribute('src');
23
+ else img.setAttribute('src', s);
24
+ }
25
+ });
26
+ ctx.dataUnsubs.push(offUrl);
27
+ }
@@ -0,0 +1,80 @@
1
+ import { applyIdAndClass, applySize, bindEnabled, bindVisible, Builder, Ctx } from "../vue-builder";
2
+ import { InputNode } from "../vue-model";
3
+
4
+ /* ----------- Input ----------- */
5
+ export function buildInput<T extends object>(builder: Builder, node: InputNode<T, any>, ctx: Ctx<T>) {
6
+ const wrapper = document.createElement('label');
7
+ applyIdAndClass(wrapper, node);
8
+ wrapper.style.display = 'block';
9
+ if (node.label) wrapper.append(document.createTextNode(node.label + ' '));
10
+
11
+ const input = document.createElement('input');
12
+ applySize(wrapper, node.width, node.height);
13
+ applySize(input, "100%", "100%");
14
+ const current = (ctx.obj as any)[node.name];
15
+ const typeGuess =
16
+ node.inputType ??
17
+ (typeof current === 'boolean' ? 'checkbox'
18
+ : typeof current === 'number' ? 'number'
19
+ : 'text');
20
+
21
+ input.type = typeGuess === 'checkbox' ? 'checkbox'
22
+ : typeGuess === 'number' ? 'number'
23
+ : 'text';
24
+
25
+ if (typeGuess === 'checkbox') {
26
+ (input as HTMLInputElement).checked = Boolean(current);
27
+ } else if (typeGuess === 'number') {
28
+ (input as HTMLInputElement).valueAsNumber =
29
+ Number.isFinite(Number(current)) ? Number(current) : 0;
30
+ } else {
31
+ (input as HTMLInputElement).value = (current ?? '') as any as string;
32
+ }
33
+
34
+ wrapper.appendChild(input);
35
+ ctx.add(wrapper);
36
+
37
+ // visible / enable factorisés
38
+ bindVisible(node, wrapper, ctx);
39
+ bindEnabled(node, input, ctx);
40
+
41
+ // modèle -> UI
42
+ const offData = ctx.listener.listen(node.name as keyof T, (v) => {
43
+ if (typeGuess === 'checkbox') {
44
+ const nv = Boolean(v);
45
+ if ((input as HTMLInputElement).checked !== nv) (input as HTMLInputElement).checked = nv;
46
+ } else if (typeGuess === 'number') {
47
+ const nv = Number(v ?? 0);
48
+ if ((input as HTMLInputElement).valueAsNumber !== nv) (input as HTMLInputElement).valueAsNumber = nv;
49
+ } else {
50
+ const s = (v as any as string) ?? '';
51
+ if ((input as HTMLInputElement).value !== s) (input as HTMLInputElement).value = s;
52
+ }
53
+ });
54
+ ctx.dataUnsubs.push(offData);
55
+
56
+ // UI -> modèle + update
57
+ const onUser = () => {
58
+ const el = input as HTMLInputElement;
59
+ let next: any;
60
+ if (typeGuess === 'checkbox') next = el.checked;
61
+ else if (typeGuess === 'number') next = Number.isFinite(el.valueAsNumber) ? el.valueAsNumber : Number(el.value);
62
+ else next = el.value;
63
+
64
+ if (node.muted) {
65
+ ctx.listener.setSilently(node.name as keyof T, next);
66
+ if (node.update) {
67
+ (ctx.listener as any).withAllMuted
68
+ ? (ctx.listener as any).withAllMuted(() => { (ctx.obj as any)[node.update]!(); })
69
+ : (ctx.obj as any)[node.update]!();
70
+
71
+ }
72
+ } else {
73
+ (ctx.obj as any)[node.name] = next;
74
+ if (node.update) { (ctx.obj as any)[node.update]!(); }
75
+ }
76
+ };
77
+ const evt = typeGuess === 'checkbox' ? 'change' : 'input';
78
+ input.addEventListener(evt, onUser);
79
+ ctx.domUnsubs.push(() => input.removeEventListener(evt, onUser));
80
+ }
@@ -0,0 +1,31 @@
1
+ import { applyIdAndClass, applySize, bindVisibleEnabled, Builder, Ctx } from "../vue-builder";
2
+ import { LabelNode, StaticLabelNode } from "../vue-model";
3
+
4
+ /* ----------- Label ----------- */
5
+ export function buildLabel<T extends object>(builder:Builder,node: LabelNode<T, any>, ctx: Ctx<T>) {
6
+ const span = document.createElement('span');
7
+ applyIdAndClass(span, node);
8
+ applySize(span, node.width, node.height);
9
+ span.textContent = String((ctx.obj as any)[node.name] ?? '');
10
+ ctx.add(span);
11
+
12
+ bindVisibleEnabled(node, span, ctx);
13
+
14
+ const offData = ctx.listener.listen(node.name as keyof T, (v) => {
15
+ const s = String(v ?? '');
16
+ if (span.textContent !== s) span.textContent = s;
17
+ });
18
+ ctx.dataUnsubs.push(offData);
19
+ }
20
+
21
+ /* ----------- Static Label ----------- */
22
+ export function buildStaticLabel<T extends object>(builder:Builder,node: StaticLabelNode<T>, ctx: Ctx<T>) {
23
+ const span = document.createElement('span');
24
+ applyIdAndClass(span, node);
25
+ applySize(span, node.width, node.height);
26
+ span.textContent = node.label;
27
+ ctx.add(span);
28
+
29
+ bindVisibleEnabled(node, span, ctx);
30
+
31
+ }
@@ -0,0 +1,65 @@
1
+ import { applyIdAndClass, applySize, Builder, Ctx, VueRuntime } from "../vue-builder";
2
+ import { ListVueNode, Vue } from "../vue-model";
3
+
4
+ /* ----------- List UI (liste d'objets) ----------- */
5
+ export function buildListOfVue<T extends object>(builder: Builder, node: ListVueNode<T>, ctx: Ctx<T>) {
6
+ const div = document.createElement('div');
7
+ applyIdAndClass(div, node);
8
+ div.style.display = 'flex';
9
+ div.style.flexDirection = (node.orientation ?? 'column') === 'row' ? 'row' : 'column';
10
+ applySize(div, node.width, node.height);
11
+
12
+ if (node.gap !== undefined) (div.style as any).gap = typeof node.gap === 'number' ? `${node.gap}px` : String(node.gap);
13
+ const mapJustify: Record<string, string> = {
14
+ start: 'flex-start', end: 'flex-end', center: 'center',
15
+ 'space-between': 'space-between', 'space-around': 'space-around', 'space-evenly': 'space-evenly'
16
+ };
17
+ const mapAlign: Record<string, string> = {
18
+ start: 'flex-start', end: 'flex-end', center: 'center', stretch: 'stretch'
19
+ };
20
+ if (node.justify) div.style.justifyContent = mapJustify[node.justify];
21
+ if (node.align) div.style.alignItems = mapAlign[node.align];
22
+ if (node.wrap) div.style.flexWrap = 'wrap';
23
+ if (node.style) for (const k of Object.keys(node.style) as Array<keyof CSSStyleDeclaration>) {
24
+ const v = node.style[k]; if (v != null) (div.style as any)[k] = v as any;
25
+ }
26
+ if (node.panel) div.classList.add('panel');
27
+
28
+ const children: VueRuntime<any>[] = [];
29
+ let initial = true;
30
+
31
+ const clear = () => {
32
+ for (const r of children) { try { r.stop(); } catch { } }
33
+ children.length = 0;
34
+ while (div.firstChild) div.removeChild(div.firstChild);
35
+ };
36
+
37
+ const render = () => {
38
+ clear();
39
+ const arr = ((ctx.obj as any)[node.list] ?? []) as any[];
40
+ for (const item of arr) {
41
+ const ui = builder.findVueFor(item);
42
+ if (!ui) continue;
43
+ const host = document.createElement('div');
44
+ if (node.wrap) {
45
+ host.style.boxSizing = "border-box";
46
+ host.style.flex = "1 1 12rem";
47
+ }
48
+ if (node.elementStyle) for (const k of Object.keys(node.elementStyle) as Array<keyof CSSStyleDeclaration>) {
49
+ const v = node.elementStyle[k]; if (v != null) (host.style as any)[k] = v as any;
50
+ }
51
+ applySize(host, node.elementWidth, node.elementHeight);
52
+ div.appendChild(host);
53
+ const runtime = builder.bootInto(ui as Vue<any>, item, host, initial ? ctx.postInits : undefined);
54
+ children.push(runtime);
55
+ }
56
+ initial = false;
57
+ };
58
+
59
+ render();
60
+ const off = ctx.listener.listen(node.list as keyof T, () => render());
61
+ ctx.dataUnsubs.push(off);
62
+ ctx.domUnsubs.push(() => clear());
63
+
64
+ ctx.add(div);
65
+ }