tauri-kargo-tools 0.1.3 → 0.1.4

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,206 @@
1
+ import { applyIdAndClass, applySize, bindVisibleEnabled, Builder, clamp, Ctx, VueRuntime } from "../vue-builder";
2
+ import { MenuNode, Vue } from "../vue-model";
3
+ /* ----------- Menu (modal <dialog> top-layer, placement précis, clics transmis aux items) ----------- */
4
+
5
+ export function buildMenu<T extends object>(builder: Builder, node: MenuNode<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
+ // <dialog> modal (top-layer) pour capturer focus/clavier et bloquer l’arrière-plan
14
+ const pop = document.createElement('dialog') as HTMLDialogElement;
15
+ pop.setAttribute('data-menu', '');
16
+ if (!document.getElementById('menu-dialog-backdrop-style')) {
17
+ const st = document.createElement('style');
18
+ st.id = 'menu-dialog-backdrop-style';
19
+ st.textContent = `dialog[data-menu]::backdrop{background:transparent !important}`;
20
+ document.head.appendChild(st);
21
+ }
22
+ pop.style.position = 'fixed';
23
+ pop.style.inset = 'auto';
24
+ pop.style.margin = '0';
25
+ pop.style.padding = '0';
26
+ pop.style.border = 'none';
27
+ pop.style.background = 'transparent';
28
+ pop.style.overflow = 'visible';
29
+ pop.style.zIndex = '2147483647';
30
+ pop.style.display = 'none';
31
+ pop.style.visibility = 'hidden';
32
+ pop.tabIndex = -1;
33
+
34
+ // Panneau visuel
35
+ const panel = document.createElement('div');
36
+ applyIdAndClass(panel, node);
37
+ applySize(panel, node.width, node.height);
38
+ panel.setAttribute('role', 'menu');
39
+ panel.setAttribute('class', 'app');
40
+ panel.tabIndex = -1;
41
+ panel.style.maxWidth = 'min(90vw, 640px)';
42
+ panel.style.maxHeight = '80vh';
43
+ panel.style.overflow = 'auto';
44
+ panel.style.boxSizing = 'border-box';
45
+ panel.style.background = 'var(--menu-bg, #fff)';
46
+ panel.style.border = '1px solid var(--menu-border, rgba(0,0,0,.12))';
47
+ panel.style.borderRadius = '8px';
48
+ panel.style.boxShadow = '0 8px 30px rgba(0,0,0,.2)';
49
+ panel.style.padding = '0';
50
+
51
+ const host = document.createElement('div');
52
+ host.style.minWidth = '220px';
53
+ panel.appendChild(host);
54
+ pop.appendChild(panel);
55
+
56
+ document.body.appendChild(pop);
57
+ ctx.domUnsubs.push(() => { try { pop.close(); pop.remove(); } catch { } });
58
+
59
+ let child: VueRuntime<any> | null = null;
60
+ let openState = false;
61
+ const cleanup: Array<() => void> = [];
62
+
63
+ const clearChild = () => {
64
+ if (child) { try { child.stop(); } catch { } child = null; }
65
+ while (host.firstChild) host.removeChild(host.firstChild);
66
+ };
67
+ const mountFor = (value: any) => {
68
+ clearChild();
69
+ const ui = builder.findVueFor(value);
70
+ if (!ui) return false;
71
+ const wrap = document.createElement('div');
72
+ host.appendChild(wrap);
73
+ child = builder.bootInto(ui as Vue<any>, value, wrap);
74
+ return true;
75
+ };
76
+
77
+ const place = () => {
78
+ pop.style.display = 'block';
79
+ pop.style.visibility = 'hidden';
80
+ if (!pop.open) { try { pop.showModal(); } catch { } } // top-layer actif
81
+
82
+ const r = btn.getBoundingClientRect();
83
+ const pw = panel.offsetWidth || 0;
84
+ const ph = panel.offsetHeight || 0;
85
+ const vw = window.innerWidth;
86
+ const vh = window.innerHeight;
87
+ const gap = 8;
88
+
89
+ let top = r.bottom + gap;
90
+ let left = r.left;
91
+ if (top + ph > vh) {
92
+ const above = r.top - gap - ph;
93
+ if (above >= 0) top = above;
94
+ }
95
+ left = clamp(left, gap, vw - pw - gap);
96
+ top = clamp(top, gap, vh - ph - gap);
97
+
98
+ pop.style.left = `${left}px`;
99
+ pop.style.top = `${top}px`;
100
+ pop.style.visibility = 'visible';
101
+ };
102
+
103
+ // Clic hors du panneau (sur le backdrop du dialog) => fermer
104
+ pop.addEventListener('click', (e) => {
105
+ if (e.target === pop) { e.preventDefault(); e.stopPropagation(); close(); }
106
+ });
107
+
108
+ // Gestion des clics **dans** le panel : laisser passer aux cibles, puis fermer
109
+ const onInsideClick = (e: MouseEvent) => {
110
+ const t = e.target as HTMLElement | null;
111
+ if (!t) return;
112
+ const item = t.closest('[data-menu-close], [role="menuitem"], button, a');
113
+ if (!item) return;
114
+ setTimeout(() => close(), 0);
115
+ };
116
+ panel.addEventListener('click', onInsideClick);
117
+
118
+ // Capture clavier au niveau du dialog (le parent ne reçoit rien)
119
+ const getFocusables = () =>
120
+ Array.from(panel.querySelectorAll<HTMLElement>(
121
+ '[role="menuitem"],button,a[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'
122
+ )).filter(el => !el.hasAttribute('disabled') && el.offsetParent !== null);
123
+
124
+ const onKey = (e: KeyboardEvent) => {
125
+ if (e.key === 'Escape') {
126
+ e.preventDefault(); e.stopPropagation();
127
+ close();
128
+ return;
129
+ }
130
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
131
+ e.preventDefault(); e.stopPropagation();
132
+ const list = getFocusables();
133
+ if (!list.length) return;
134
+ const idx = list.indexOf(document.activeElement as HTMLElement);
135
+ const dir = (e.key === 'ArrowDown') ? 1 : -1;
136
+ const next = list[(idx + dir + list.length) % list.length] || list[0];
137
+ next.focus();
138
+ return;
139
+ }
140
+ e.stopPropagation();
141
+ };
142
+
143
+ const close = () => {
144
+ if (!openState) return;
145
+ for (const f of cleanup.splice(0)) { try { f(); } catch { } }
146
+ try { pop.close(); } catch { }
147
+ pop.style.display = 'none';
148
+ pop.style.visibility = 'hidden';
149
+ clearChild();
150
+ openState = false;
151
+ };
152
+
153
+ const open = () => {
154
+ if (openState) { close(); return; }
155
+ if (node.action) { try { (ctx.obj as any)[node.action]!(); } catch { } }
156
+ const value = (ctx.obj as any)[node.name];
157
+ if (value == null) { close(); return; }
158
+ if (!mountFor(value)) return;
159
+
160
+ place();
161
+ requestAnimationFrame(() => {
162
+ place();
163
+ const list = getFocusables();
164
+ (list[0] ?? panel).focus({ preventScroll: true });
165
+ });
166
+
167
+ document.addEventListener('keydown', onKey, true);
168
+ const onResize = () => place();
169
+ const onScroll = () => place();
170
+ window.addEventListener('resize', onResize);
171
+ window.addEventListener('scroll', onScroll, true);
172
+
173
+ cleanup.push(() => {
174
+ document.removeEventListener('keydown', onKey, true);
175
+ window.removeEventListener('resize', onResize);
176
+ window.removeEventListener('scroll', onScroll, true);
177
+ });
178
+
179
+ if (!(node.closeOnEsc ?? true)) {
180
+ const onCancel = (e: Event) => e.preventDefault();
181
+ pop.addEventListener('cancel', onCancel);
182
+ cleanup.push(() => pop.removeEventListener('cancel', onCancel));
183
+ }
184
+
185
+ openState = true;
186
+ };
187
+
188
+ // Réactivité si la source change pendant l’ouverture
189
+ const offField = ctx.listener.listen(node.name as keyof T, (v) => {
190
+ if (v == null) close();
191
+ else if (openState) {
192
+ const s = panel.scrollTop;
193
+ mountFor(v);
194
+ place();
195
+ panel.scrollTop = s;
196
+ }
197
+ });
198
+ ctx.dataUnsubs.push(offField);
199
+
200
+ // visible / enable (bouton)
201
+ bindVisibleEnabled(node, btn, ctx);
202
+
203
+ btn.addEventListener('click', open);
204
+ ctx.domUnsubs.push(() => btn.removeEventListener('click', open));
205
+ ctx.domUnsubs.push(() => close());
206
+ }
@@ -0,0 +1,88 @@
1
+ import { applyIdAndClass, applySize, bindVisibleEnabled, Builder, Ctx } from "../vue-builder";
2
+ import { SelectNode } from "../vue-model";
3
+
4
+ /* ----------- Select ----------- */
5
+ export function buildSelect<T extends object>(builder: Builder, node: SelectNode<T, any, any, any, any>, ctx: Ctx<T>) {
6
+ const sel = document.createElement('select');
7
+ applyIdAndClass(sel, node);
8
+
9
+ // NEW: mode handling
10
+ const mode = (node.mode ?? 'list'); // 'dropdown' | 'list' | 'multi-list'
11
+ sel.multiple = mode === 'multi-list'; // multi seulement en 'multi-list'
12
+
13
+ applySize(sel, node.width, node.height);
14
+ ctx.add(sel);
15
+
16
+ bindVisibleEnabled(node, sel, ctx);
17
+
18
+ let displayFn = (ctx.obj as any)[node.displayMethod] as (a: any) => string;
19
+ if (typeof displayFn === 'function') displayFn = displayFn.bind(ctx.obj);
20
+
21
+ // NEW: helper pour calculer la hauteur visible (nombre de lignes)
22
+ const computeListSize = (count: number) => {
23
+ if (mode === 'dropdown') return 1;
24
+ // Par défaut on montre entre 2 et 10 lignes (sans dépasser la taille)
25
+ return Math.max(Math.min(count, 10), 2);
26
+ };
27
+
28
+ const rebuild = () => {
29
+ const arr = ((ctx.obj as any)[node.list] ?? []) as any[];
30
+ while (sel.firstChild) sel.removeChild(sel.firstChild);
31
+ for (let i = 0; i < arr.length; i++) {
32
+ const opt = document.createElement('option');
33
+ opt.value = String(i);
34
+ try { opt.text = String(displayFn(arr[i])); }
35
+ catch { opt.text = String(arr[i] as unknown as string); }
36
+ sel.appendChild(opt);
37
+ }
38
+
39
+ // NEW: forcer l’apparence "liste" quand mode != dropdown
40
+ sel.size = computeListSize(arr.length);
41
+ };
42
+
43
+ const syncSelection = () => {
44
+ const selectedIdx = (((ctx.obj as any)[node.selection] ?? []) as number[])
45
+ .filter((n: any) => Number.isFinite(n)) as number[];
46
+
47
+ if (sel.multiple) {
48
+ const set = new Set(selectedIdx);
49
+ for (const opt of Array.from(sel.options)) {
50
+ const idx = Number(opt.value);
51
+ opt.selected = set.has(idx);
52
+ }
53
+ } else {
54
+ const first = selectedIdx.find((n) => n >= 0 && n < sel.options.length);
55
+ if (first !== undefined) sel.value = String(first);
56
+ else sel.selectedIndex = -1;
57
+ }
58
+ };
59
+
60
+ rebuild();
61
+ syncSelection();
62
+
63
+ const offList = ctx.listener.listen(node.list as keyof T, () => { rebuild(); syncSelection(); });
64
+ const offSel = ctx.listener.listen(node.selection as keyof T, () => { syncSelection(); });
65
+ ctx.dataUnsubs.push(offList, offSel);
66
+
67
+ const onChange = () => {
68
+ let indices: number[];
69
+ if (sel.multiple) {
70
+ indices = Array.from(sel.selectedOptions)
71
+ .map(o => Number(o.value))
72
+ .filter(n => Number.isFinite(n));
73
+ } else {
74
+ indices = (sel.selectedIndex >= 0) ? [Number(sel.value)] : [];
75
+ }
76
+ if (node.muted) {
77
+ ctx.listener.setSilently(node.selection as keyof T, indices as any);
78
+ (ctx.listener as any).withAllMuted
79
+ ? (ctx.listener as any).withAllMuted(() => { (ctx.obj as any)[node.update]!(); })
80
+ : (ctx.obj as any)[node.update]!();
81
+ } else {
82
+ (ctx.obj as any)[node.selection] = indices as any;
83
+ (ctx.obj as any)[node.update]!();
84
+ }
85
+ };
86
+ sel.addEventListener('change', onChange);
87
+ ctx.domUnsubs.push(() => sel.removeEventListener('change', onChange));
88
+ }
@@ -0,0 +1,34 @@
1
+ import { applyIdAndClass, applySize, Builder, Ctx, VueRuntime } from "../vue-builder";
2
+ import { SingleVueNode, Vue } from "../vue-model";
3
+
4
+ /* ----------- Single UI (champ objet) ----------- */
5
+ export function buildSingleVue<T extends object>(builder:Builder,node: SingleVueNode<T>, ctx: Ctx<T>) {
6
+ const host = document.createElement('div');
7
+ applyIdAndClass(host, node);
8
+ applySize(host, node.width, node.height);
9
+ ctx.add(host);
10
+
11
+ let child: VueRuntime<any> | null = null;
12
+
13
+ const clearHost = () => {
14
+ if (child) { try { child.stop(); } catch { } child = null; }
15
+ while (host.firstChild) host.removeChild(host.firstChild);
16
+ };
17
+
18
+ const mountFor = (value: any, duringBuild: boolean) => {
19
+ clearHost();
20
+ const ui = builder.findVueFor(value);
21
+ if (!ui) return;
22
+ const inner = document.createElement('div');
23
+ host.appendChild(inner);
24
+ child = builder.bootInto(ui as Vue<any>, value, inner, duringBuild ? ctx.postInits : undefined);
25
+ };
26
+
27
+ // initial (defer init au parent)
28
+ mountFor((ctx.obj as any)[node.name], true);
29
+
30
+ // updates (exécuter inits immédiatement)
31
+ const off = ctx.listener.listen(node.name as keyof T, (v) => mountFor(v, false));
32
+ ctx.dataUnsubs.push(off);
33
+ ctx.domUnsubs.push(() => clearHost());
34
+ }
@@ -0,0 +1,75 @@
1
+ interface Holder<T> {
2
+ target?: T
3
+ }
4
+
5
+
6
+ function createProxy<T extends object>(holder: Holder<T>) {
7
+ const proxy = new Proxy({} as T, {
8
+
9
+ get(_targetObject: T, property: string | symbol, receiver) {
10
+
11
+ const value = Reflect.get(holder.target!, property, receiver);
12
+
13
+ if (typeof value === "function") {
14
+ return function <U>(...args: U[]) {
15
+
16
+ return value.apply(holder.target, args);
17
+ };
18
+ }
19
+ return value;
20
+ },
21
+ set(_targetObject: T, property: string | symbol, value, receiver) {
22
+ // Redirige les écritures vers la cible actuelle
23
+ console.log(`Setting property: ${String(property)} to ${value}`);
24
+ return Reflect.set(holder.target!, property, value, receiver);
25
+ }
26
+ });
27
+ return proxy
28
+
29
+
30
+ }
31
+
32
+
33
+ class Container {
34
+ map!: Map<unknown, unknown>
35
+ mapProxy: Map<unknown, boolean>
36
+ constructor() {
37
+ this.map = new Map()
38
+ this.mapProxy = new Map()
39
+ }
40
+ getInstance<Type extends object>(typeConstructor: new () => Type): Type {
41
+ let c = this.map.get(typeConstructor) as Type
42
+ if (!c) {
43
+ const holder: Holder<Type> = {}
44
+ const proxy = createProxy<Type>(holder)
45
+ this.map.set(typeConstructor, proxy)
46
+ this.mapProxy.set(proxy, false)
47
+ c = new typeConstructor()
48
+ const proxyUsed = this.mapProxy.get(proxy)!
49
+ if (!proxyUsed) {
50
+ this.map.set(typeConstructor, c)
51
+ this.mapProxy.delete(proxy)
52
+ } else {
53
+ holder.target = c
54
+ }
55
+ return c
56
+ }
57
+ if (this.mapProxy.has(c)) {
58
+ this.mapProxy.set(c, true)
59
+ }
60
+ return c
61
+
62
+
63
+ }
64
+ setInstance<Type extends object>(value: Type) {
65
+ this.map.set(value.constructor, value)
66
+ }
67
+
68
+ }
69
+ const container: Container = new Container()
70
+ export function get<Type extends object>(typeConstructor: new () => Type): Type {
71
+ return container.getInstance(typeConstructor)
72
+ }
73
+ export function set<Type extends object>(value: Type) {
74
+ container.setInstance(value)
75
+ }
@@ -0,0 +1,41 @@
1
+ // listener-factory.ts
2
+ import { Listener, Handler, Unlisten } from "./listener";
3
+
4
+ /** Cache: 1 Listener par objet */
5
+ const LISTENER_CACHE: WeakMap<object, Listener<any>> = new WeakMap();
6
+
7
+ /** Récupère (ou crée) le Listener associé à obj */
8
+ export function getListener<T extends object>(obj: T): Listener<T> {
9
+ let l = LISTENER_CACHE.get(obj) as Listener<T> | undefined;
10
+ if (!l) {
11
+ l = new Listener(obj);
12
+ LISTENER_CACHE.set(obj, l);
13
+ }
14
+ return l;
15
+ }
16
+
17
+ /** Optionnel: helpers pratiques */
18
+ export function on<T extends object, K extends keyof T>(
19
+ obj: T,
20
+ key: K,
21
+ cb: Handler<T, K>
22
+ ): Unlisten {
23
+ return getListener(obj).listen(key, cb);
24
+ }
25
+
26
+ export function setSilently<T extends object, K extends keyof T>(
27
+ obj: T,
28
+ key: K,
29
+ value: T[K]
30
+ ) {
31
+ getListener(obj).setSilently(key, value);
32
+ }
33
+
34
+ export function stopListener(obj: object) {
35
+ const l = LISTENER_CACHE.get(obj);
36
+ if (l) { l.stop(); LISTENER_CACHE.delete(obj); }
37
+ }
38
+
39
+ export function hasListener(obj: object): boolean {
40
+ return LISTENER_CACHE.has(obj);
41
+ }
@@ -0,0 +1,134 @@
1
+ export type Unlisten = () => void;
2
+ export type Handler<T, K extends keyof T> = (value: T[K], old: T[K]) => void;
3
+
4
+ export class Listener<T extends object> {
5
+ protected obj: T;
6
+
7
+ /** État par propriété instrumentée. La valeur courante est toujours `value`. */
8
+ protected fields = new Map<PropertyKey, {
9
+ prevDesc?: PropertyDescriptor; // descripteur propre d'origine (si existait sur l'instance)
10
+ handlers: Set<(v: any, o: any) => void>; // callbacks
11
+ enumerable: boolean; // pour restaurer proprement
12
+ value: any; // valeur source-de-vérité tant que c'est instrumenté
13
+ muted: number; // compteur de mute (réentrant)
14
+ }>();
15
+
16
+ constructor(obj: T) { this.obj = obj; }
17
+
18
+ listen<K extends keyof T>(key: K, handler: Handler<T, K>): Unlisten {
19
+ const prop = key as PropertyKey;
20
+ let st = this.fields.get(prop);
21
+
22
+ if (!st) {
23
+ // Descripteur propre s'il existe (sur l'instance)
24
+ const own = Object.getOwnPropertyDescriptor(this.obj, prop as any);
25
+
26
+ if (own && own.configurable === false) {
27
+ throw new Error(`Le champ "${String(prop)}" est non configurable.`);
28
+ }
29
+
30
+ const enumerable = own?.enumerable ?? true;
31
+
32
+ // Valeur initiale à capturer AVANT de remplacer par notre accessor
33
+ let current: any;
34
+ if (own) {
35
+ // data property
36
+ if ("value" in own) current = own.value;
37
+ // accessor property
38
+ else current = own.get ? own.get.call(this.obj) : undefined;
39
+ } else {
40
+ // rien sur l'instance → on lit (prototype/valeur courante)
41
+ current = (this.obj as any)[prop];
42
+ }
43
+
44
+ st = {
45
+ prevDesc: own,
46
+ handlers: new Set<(v: any, o: any) => void>(),
47
+ enumerable,
48
+ value: current,
49
+ muted: 0,
50
+ };
51
+
52
+ // On pose NOTRE accessor sur l'instance : get/set utilisent toujours st.value
53
+ Object.defineProperty(this.obj, prop, {
54
+ configurable: true,
55
+ enumerable,
56
+ get: () => st!.value,
57
+ set: (newVal: any) => {
58
+ const oldVal = st!.value;
59
+ st!.value = newVal;
60
+ if (st!.muted > 0 || Object.is(oldVal, newVal)) return;
61
+ for (const h of st!.handlers) {
62
+ try { h(newVal, oldVal); } catch { /* ignore handler errors */ }
63
+ }
64
+ }
65
+ });
66
+
67
+ this.fields.set(prop, st);
68
+ }
69
+
70
+ st.handlers.add(handler as any);
71
+ return () => this.unlisten(key, handler);
72
+ }
73
+
74
+ unlisten<K extends keyof T>(key: K, handler: Handler<T, K>): boolean {
75
+ const prop = key as PropertyKey;
76
+ const st = this.fields.get(prop);
77
+ if (!st) return false;
78
+
79
+ const ok = st.handlers.delete(handler as any);
80
+ if (st.handlers.size === 0) {
81
+ this.restore(prop, st);
82
+ this.fields.delete(prop);
83
+ }
84
+ return ok;
85
+ }
86
+
87
+ stop(): void {
88
+ for (const [prop, st] of this.fields) this.restore(prop, st);
89
+ this.fields.clear();
90
+ }
91
+
92
+ /** Écrit sans notifier */
93
+ setSilently<K extends keyof T>(key: K, value: T[K]) {
94
+ this.withMuted(key, () => { (this.obj as any)[key] = value; });
95
+ }
96
+
97
+ /** Mute une propriété (si instrumentée) pendant l'exécution de `fn` */
98
+ protected withMuted<K extends keyof T>(key: K, fn: () => void) {
99
+ const st = this.fields.get(key as PropertyKey);
100
+ if (!st) { fn(); return; } // pas instrumentée → exécuter sans mute
101
+ try { st.muted++; fn(); } finally { st.muted--; }
102
+ }
103
+
104
+ /** Mute toutes les propriétés instrumentées pendant l'exécution de `fn` */
105
+ protected withAllMuted(fn: () => void) {
106
+ try {
107
+ for (const st of this.fields.values()) st.muted++;
108
+ fn();
109
+ } finally {
110
+ for (const st of this.fields.values()) st.muted--;
111
+ }
112
+ }
113
+
114
+ /** Restaure la propriété sur l'instance et tente de conserver la dernière valeur */
115
+ private restore(prop: PropertyKey, st: NonNullable<ReturnType<typeof this.fields.get>>) {
116
+ const last = st.value;
117
+
118
+ if (st.prevDesc) {
119
+ // Remet le descripteur d’origine sur l'instance
120
+ Object.defineProperty(this.obj, prop, st.prevDesc);
121
+ // Essaye d’y réinjecter la dernière valeur (si setter/data writable)
122
+ try { (this.obj as any)[prop] = last; } catch { /* readonly d'origine, on ignore */ }
123
+ } else {
124
+ // Aucune prop propre avant : on remet une data property standard avec la dernière valeur
125
+ delete (this.obj as any)[prop];
126
+ Object.defineProperty(this.obj, prop, {
127
+ configurable: true,
128
+ enumerable: st.enumerable,
129
+ writable: true,
130
+ value: last
131
+ });
132
+ }
133
+ }
134
+ }