@jsswift/core 1.0.41

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,393 @@
1
+ (() => {
2
+
3
+ /* ===============================
4
+ JSswift core mini (ready + dom + reactive)
5
+ =============================== */
6
+
7
+ window.JSswift = window.JSswift || {};
8
+ const JSswift = window.JSswift;
9
+ window._ = window.JSswift; // legacy alias, deprecated: use `_.`
10
+
11
+
12
+ JSswift.config = JSswift.config || {};
13
+ const JSswiftBootstrapSetting =
14
+ typeof globalThis !== "undefined" && globalThis.JSswift_setting
15
+ ? globalThis.JSswift_setting
16
+ : {};
17
+ JSswift.config.debug = JSswiftBootstrapSetting.modeDev ?? false;
18
+
19
+ JSswift.isDev = function () {
20
+ return JSswift.config.debug;
21
+ };
22
+
23
+ // UTILITIES
24
+ JSswift.uiNormalizeArgs = function (args) {
25
+ let props = {};
26
+ let children = args;
27
+
28
+ if (
29
+ args.length > 0 &&
30
+ typeof args[0] === "object" &&
31
+ args[0] !== null &&
32
+ !args[0].nodeType &&
33
+ !Array.isArray(args[0]) &&
34
+ !(args[0] instanceof Function)
35
+ ) {
36
+ props = args[0];
37
+ children = args.slice(1);
38
+ }
39
+ return { props, children };
40
+ }
41
+ JSswift.uiSizes = ["xxs", "xs", "sm", "md", "lg", "xl", "xxl", "xxxl"];
42
+ JSswift.uiColors = ["primary", "secondary", "success", "warning", "danger", "info", "light", "dark"];
43
+ JSswift.meta = JSswift.meta || {
44
+ version: "1.0.23",
45
+ policy: {
46
+ sourceOfTruth: "docs/reference/core.md",
47
+ syncRule: "Quando cambia un modulo core, aggiornare sia questo meta sia docs/reference/core.md."
48
+ },
49
+ modules: {
50
+ render: {
51
+ description: "Bridge props -> DOM per hyperscript _, attributi, class, style, eventi e children reattivi.",
52
+ entrypoints: ["createElement", "setProp", "bindProp"],
53
+ status: "milestone-3-closed",
54
+ knownLimits: [
55
+ "Il mini-terzo-giro e il successivo micro-giro avanzato hanno chiuso style dinamico, eventi, children e props/attr speciali, ma restano altri edge case avanzati da esplorare.",
56
+ "Gli eventi ora supportano composizione base di listener multipli, ma non hanno ancora delegation o diff avanzato.",
57
+ "Restano da esplorare edge case avanzati del renderer oltre ai giri gia coperti dai test."
58
+ ]
59
+ },
60
+ reactive: {
61
+ description: "Core minimale signal/effect usato come base della reattivita.",
62
+ entrypoints: ["JSswift.reactive.signal", "JSswift.reactive.effect", "JSswift.reactive.computed", "JSswift.reactive.untracked", "JSswift.reactive.batch"],
63
+ status: "milestone-2-closed",
64
+ knownLimits: [
65
+ "La protezione loop copre i loop sincroni per singolo effect, non ancora i cicli complessi tra effect multipli.",
66
+ "Il batching sincrono ora esiste, ma manca ancora scheduling configurabile oltre il flush immediato a fine batch.",
67
+ "Mancano primitive avanzate per controllare scheduling e transazioni asincrone."
68
+ ]
69
+ },
70
+ rod: {
71
+ description: "Layer reattivo di alto livello per binding DOM, model e interpolazioni.",
72
+ entrypoints: ["_.rod", "JSswift.rodBind", "JSswift.rodModel"],
73
+ status: "milestone-2-closed",
74
+ knownLimits: [
75
+ "Va chiarito meglio il rapporto con JSswift.reactive oltre al primo riallineamento strutturale.",
76
+ "Restano casi avanzati di model/binding da esplorare oltre al primo giro coperto dai test.",
77
+ "Il modulo e piu pulito internamente ma non e ancora il punto finale della convergenza con il renderer."
78
+ ]
79
+ },
80
+ mount: {
81
+ description: "Mount, component instances e cleanup automatico del tree DOM.",
82
+ entrypoints: ["JSswift.mount", "JSswift.component", "JSswift.enableAutoCleanup"],
83
+ status: "milestone-2-closed",
84
+ knownLimits: [
85
+ "Il lifecycle e piu pulito internamente ma resta da chiarire meglio la semantica su multi-mount e cloni.",
86
+ "Restano da esplorare edge case avanzati del cleanup automatico fuori dal primo giro gia coperto."
87
+ ]
88
+ },
89
+ platform: {
90
+ description: "Moduli applicativi nel core: overlay, store, auth, http, router, UI meta.",
91
+ entrypoints: ["JSswift.overlay", "JSswift.store", "JSswift.plugins.auth", "JSswift.http", "JSswift.router", "JSswift.ui.meta"],
92
+ status: "milestone-2-closed",
93
+ knownLimits: [
94
+ "Il secondo giro ha ripulito i moduli interni, ma mancano ancora demo separate per modulo e una validazione piu formale di alcune superfici pubbliche.",
95
+ "Esiste una demo browser aggregata del blocco platform, ma non ancora demo separate per ogni modulo.",
96
+ "Mancano configurazione pubblica piu coerente e confini piu netti tra auth, http e router.",
97
+ "Il registry UI meta non ha ancora validazione formale del suo shape."
98
+ ]
99
+ }
100
+ }
101
+ };
102
+
103
+ JSswift.omit = (obj, keys) => {
104
+ const out = {};
105
+ const skip = new Set(keys || []);
106
+ for (const k in (obj || {})) {
107
+ if (!skip.has(k)) out[k] = obj[k];
108
+ }
109
+ return out;
110
+ };
111
+
112
+
113
+ // ===============================
114
+ // Cleanup registry (DOM -> disposers)
115
+ // ===============================
116
+ JSswift._cleanupRegistry = new WeakMap();
117
+ JSswift._registerCleanup = function (node, disposer) {
118
+ if (!node || typeof disposer !== "function") return disposer;
119
+ const list = JSswift._cleanupRegistry.get(node) || [];
120
+ list.push(disposer);
121
+ JSswift._cleanupRegistry.set(node, list);
122
+ return disposer;
123
+ };
124
+
125
+
126
+ JSswift.dom = {
127
+ q(sel, root = document) { return root.querySelector(sel); },
128
+ qa(sel, root = document) { return Array.from(root.querySelectorAll(sel)); },
129
+ attr(el, name, value) {
130
+ if (!el) return null;
131
+ if (value === undefined) return el.getAttribute(name);
132
+ if (value === null) el.removeAttribute(name);
133
+ else el.setAttribute(name, String(value));
134
+ return el;
135
+ }
136
+ };
137
+
138
+ // READY definitivo
139
+ (() => {
140
+ let _queue = [];
141
+ let _ready = false;
142
+
143
+ function flushReady() {
144
+ if (_ready) return;
145
+ _ready = true;
146
+ const q = _queue.slice();
147
+ _queue.length = 0;
148
+ for (const fn of q) {
149
+ try { fn(); } catch (e) { console.error("[JSswift.ready] error:", e); }
150
+ }
151
+ }
152
+
153
+ JSswift.ready = function (fn) {
154
+ if (typeof fn !== "function") return;
155
+ if (_ready || document.readyState === "interactive" || document.readyState === "complete") {
156
+ queueMicrotask(() => {
157
+ try { fn(); } catch (e) { console.error("[JSswift.ready] error:", e); }
158
+ });
159
+ } else {
160
+ _queue.push(fn);
161
+ }
162
+ };
163
+
164
+ document.addEventListener("DOMContentLoaded", flushReady, { once: true });
165
+ })();
166
+
167
+ // REACTIVE core
168
+ JSswift.reactive = (() => {
169
+ let CURRENT_EFFECT = null;
170
+ const EFFECT_STACK = [];
171
+ const MAX_SYNC_EFFECT_RERUNS = 100;
172
+ let BATCH_DEPTH = 0;
173
+ let BATCH_FLUSH_MODE = "sync";
174
+ let PENDING_FLUSH_MODE = "sync";
175
+ let MICROTASK_FLUSH_SCHEDULED = false;
176
+ const PENDING_RUNNERS = new Set();
177
+
178
+ function normalizeFlushMode(options) {
179
+ const mode = options?.flush;
180
+ if (mode == null || mode === "sync") return "sync";
181
+ if (mode === "microtask") return "microtask";
182
+ throw new Error("JSswift.reactive.batch: options.flush must be 'sync' or 'microtask'");
183
+ }
184
+
185
+ function flushPendingRunners() {
186
+ while (PENDING_RUNNERS.size) {
187
+ const jobs = Array.from(PENDING_RUNNERS);
188
+ PENDING_RUNNERS.clear();
189
+ jobs.forEach((fn) => fn());
190
+ }
191
+ }
192
+
193
+ function scheduleMicrotaskFlush() {
194
+ if (MICROTASK_FLUSH_SCHEDULED) return;
195
+ MICROTASK_FLUSH_SCHEDULED = true;
196
+
197
+ queueMicrotask(() => {
198
+ MICROTASK_FLUSH_SCHEDULED = false;
199
+ PENDING_FLUSH_MODE = "sync";
200
+ flushPendingRunners();
201
+ });
202
+ }
203
+
204
+ function notifySubscribers(subs) {
205
+ const jobs = Array.from(subs);
206
+ if (BATCH_DEPTH > 0 || PENDING_FLUSH_MODE === "microtask") {
207
+ jobs.forEach((fn) => PENDING_RUNNERS.add(fn));
208
+ if (BATCH_DEPTH === 0 && PENDING_FLUSH_MODE === "microtask") {
209
+ scheduleMicrotaskFlush();
210
+ }
211
+ return;
212
+ }
213
+ jobs.forEach((fn) => fn());
214
+ }
215
+
216
+ function cleanupEffect(record) {
217
+ if (!record) return;
218
+
219
+ if (record._deps.size) {
220
+ record._deps.forEach((dep) => dep.delete(record._runner));
221
+ record._deps.clear();
222
+ }
223
+
224
+ if (typeof record._cleanup === "function") {
225
+ const cleanup = record._cleanup;
226
+ record._cleanup = null;
227
+ try { cleanup(); } catch (e) { console.error("[JSswift.reactive.effect] cleanup error:", e); }
228
+ }
229
+ }
230
+
231
+ function runEffect(record) {
232
+ if (!record || !record.active) return;
233
+
234
+ cleanupEffect(record);
235
+
236
+ EFFECT_STACK.push(record);
237
+ CURRENT_EFFECT = record;
238
+
239
+ try {
240
+ const onCleanup = (fn) => {
241
+ record._cleanup = typeof fn === "function" ? fn : null;
242
+ };
243
+
244
+ const out = record.fn.length > 0 ? record.fn(onCleanup) : record.fn();
245
+ if (typeof out === "function") {
246
+ record._cleanup = out;
247
+ }
248
+ } finally {
249
+ EFFECT_STACK.pop();
250
+ CURRENT_EFFECT = EFFECT_STACK[EFFECT_STACK.length - 1] || null;
251
+ }
252
+ }
253
+
254
+ function effect(fn) {
255
+ if (typeof fn !== "function") return () => { };
256
+
257
+ const record = {
258
+ fn,
259
+ active: true,
260
+ _deps: new Set(),
261
+ _cleanup: null,
262
+ _runner: null,
263
+ _running: false,
264
+ _queued: false
265
+ };
266
+
267
+ const runner = () => {
268
+ if (!record.active) return;
269
+
270
+ if (record._running) {
271
+ record._queued = true;
272
+ return;
273
+ }
274
+
275
+ record._running = true;
276
+ let reruns = 0;
277
+
278
+ try {
279
+ do {
280
+ record._queued = false;
281
+ runEffect(record);
282
+ reruns++;
283
+
284
+ if (reruns >= MAX_SYNC_EFFECT_RERUNS) {
285
+ record._queued = false;
286
+ console.warn("[JSswift.reactive.effect] loop guard triggered: too many synchronous reruns", {
287
+ max: MAX_SYNC_EFFECT_RERUNS,
288
+ effect: fn.name || "anonymous"
289
+ });
290
+ break;
291
+ }
292
+ } while (record.active && record._queued);
293
+ } finally {
294
+ record._running = false;
295
+ }
296
+ };
297
+ record._runner = runner;
298
+
299
+ runner();
300
+
301
+ return () => {
302
+ if (!record.active) return;
303
+ record.active = false;
304
+ cleanupEffect(record);
305
+ };
306
+ }
307
+
308
+ function signal(initial) {
309
+ let value = initial;
310
+ const subs = new Set();
311
+
312
+ function get() {
313
+ if (CURRENT_EFFECT && CURRENT_EFFECT.active) {
314
+ subs.add(CURRENT_EFFECT._runner);
315
+ CURRENT_EFFECT._deps.add(subs);
316
+ }
317
+ return value;
318
+ }
319
+
320
+ function set(v) {
321
+ value = v;
322
+ notifySubscribers(subs);
323
+ }
324
+
325
+ function dispose() {
326
+ subs.clear();
327
+ }
328
+
329
+ return [get, set, dispose];
330
+ }
331
+
332
+ function computed(fn) {
333
+ if (typeof fn !== "function") throw new Error("JSswift.reactive.computed: fn must be a function");
334
+
335
+ const [get, set, disposeSignal] = signal(undefined);
336
+ const disposeEffect = effect(() => {
337
+ set(fn());
338
+ });
339
+
340
+ const getter = () => get();
341
+ getter.dispose = () => {
342
+ disposeEffect();
343
+ disposeSignal();
344
+ };
345
+ getter.peek = () => get();
346
+ getter.type = "computed";
347
+
348
+ return getter;
349
+ }
350
+
351
+ function untracked(fn) {
352
+ if (typeof fn !== "function") throw new Error("JSswift.reactive.untracked: fn must be a function");
353
+
354
+ const prevEffect = CURRENT_EFFECT;
355
+ CURRENT_EFFECT = null;
356
+ try {
357
+ return fn();
358
+ } finally {
359
+ CURRENT_EFFECT = prevEffect;
360
+ }
361
+ }
362
+
363
+ function batch(fn, options) {
364
+ if (typeof fn !== "function") throw new Error("JSswift.reactive.batch: fn must be a function");
365
+ const flushMode = normalizeFlushMode(options);
366
+
367
+ if (BATCH_DEPTH === 0) {
368
+ BATCH_FLUSH_MODE = flushMode;
369
+ } else if (flushMode === "microtask") {
370
+ BATCH_FLUSH_MODE = "microtask";
371
+ }
372
+
373
+ BATCH_DEPTH += 1;
374
+ try {
375
+ return fn();
376
+ } finally {
377
+ BATCH_DEPTH -= 1;
378
+ if (BATCH_DEPTH === 0) {
379
+ const shouldUseMicrotask = BATCH_FLUSH_MODE === "microtask" || PENDING_FLUSH_MODE === "microtask";
380
+ BATCH_FLUSH_MODE = "sync";
381
+
382
+ if (shouldUseMicrotask) {
383
+ PENDING_FLUSH_MODE = "microtask";
384
+ scheduleMicrotaskFlush();
385
+ } else {
386
+ flushPendingRunners();
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ return { signal, effect, computed, untracked, batch };
393
+ })();
@@ -0,0 +1,162 @@
1
+ // ===============================
2
+ // Overlay shared helpers
3
+ // ===============================
4
+ JSswift._overlayShared = (() => {
5
+ const focusSelector = [
6
+ "button:not([disabled])",
7
+ "[href]",
8
+ "input:not([disabled])",
9
+ "select:not([disabled])",
10
+ "textarea:not([disabled])",
11
+ "[tabindex]:not([tabindex='-1'])"
12
+ ].join(",");
13
+
14
+ function ensureRoot(getRoot, setRoot) {
15
+ const currentRoot = getRoot();
16
+ if (currentRoot && currentRoot.isConnected) return currentRoot;
17
+
18
+ let el = document.getElementById("cms-overlay-root");
19
+ if (!el && document?.body) {
20
+ el = document.createElement("div");
21
+ el.id = "cms-overlay-root";
22
+ el.className = "cms-overlay-root";
23
+ document.body.appendChild(el);
24
+ }
25
+
26
+ if (!document.body && !el) {
27
+ JSswift.ready(() => {
28
+ let readyEl = document.getElementById("cms-overlay-root");
29
+ if (!readyEl) {
30
+ readyEl = document.createElement("div");
31
+ readyEl.id = "cms-overlay-root";
32
+ readyEl.className = "cms-overlay-root";
33
+ document.body.appendChild(readyEl);
34
+ }
35
+ setRoot(readyEl);
36
+ });
37
+ }
38
+
39
+ setRoot(el);
40
+ return el;
41
+ }
42
+
43
+ function focusFirst(container) {
44
+ const node = container.querySelector(focusSelector);
45
+ node?.focus?.();
46
+ }
47
+
48
+ function trapFocus(event, container) {
49
+ if (event.key !== "Tab") return;
50
+ const nodes = Array.from(container.querySelectorAll(focusSelector)).filter((node) => node.offsetParent !== null);
51
+ if (!nodes.length) return;
52
+
53
+ const first = nodes[0];
54
+ const last = nodes[nodes.length - 1];
55
+ const active = document.activeElement;
56
+
57
+ if (event.shiftKey && active === first) {
58
+ event.preventDefault();
59
+ last.focus();
60
+ } else if (!event.shiftKey && active === last) {
61
+ event.preventDefault();
62
+ first.focus();
63
+ }
64
+ }
65
+
66
+ function applyAnchoredPosition(panel, opts) {
67
+ if (!opts.anchorEl) return;
68
+
69
+ const anchorRect = opts.anchorEl.getBoundingClientRect();
70
+ const panelRect = panel.getBoundingClientRect();
71
+ const visualViewport = window.visualViewport || null;
72
+ const viewportOffsetLeft = Math.max(0, Number(visualViewport?.offsetLeft) || 0);
73
+ const viewportOffsetTop = Math.max(0, Number(visualViewport?.offsetTop) || 0);
74
+ const viewportWidth = Math.max(
75
+ Number(visualViewport?.width) || 0,
76
+ window.innerWidth || 0,
77
+ document.documentElement?.clientWidth || 0
78
+ );
79
+ const viewportHeight = Math.max(
80
+ Number(visualViewport?.height) || 0,
81
+ window.innerHeight || 0,
82
+ document.documentElement?.clientHeight || 0
83
+ );
84
+ const rawGutter = Number(opts.viewportGutter ?? opts.gutter ?? 8);
85
+ const gutter = Number.isFinite(rawGutter) ? Math.max(0, rawGutter) : 8;
86
+ const rawOffsetX = Number(opts.offsetX ?? 0);
87
+ const rawOffsetY = Number(opts.offsetY ?? 8);
88
+ const offsetX = Number.isFinite(rawOffsetX) ? rawOffsetX : 0;
89
+ const offsetY = Number.isFinite(rawOffsetY) ? rawOffsetY : 8;
90
+
91
+ const clamp = (value, min, max) => {
92
+ if (!Number.isFinite(value)) return min;
93
+ if (!Number.isFinite(min)) min = 0;
94
+ if (!Number.isFinite(max)) max = min;
95
+ if (max < min) return min;
96
+ return Math.min(Math.max(value, min), max);
97
+ };
98
+
99
+ const rawPlacement = String(opts.placement || "bottom-start").toLowerCase();
100
+ const [rawSide, rawAlign] = rawPlacement.split("-");
101
+ const side = ["top", "bottom", "left", "right"].includes(rawSide) ? rawSide : "bottom";
102
+ const align = ["start", "end", "center"].includes(rawAlign) ? rawAlign : "start";
103
+ const oppositeSide = { top: "bottom", bottom: "top", left: "right", right: "left" }[side] || "top";
104
+ const isVertical = side === "top" || side === "bottom";
105
+ const panelWidth = panelRect.width;
106
+ const panelHeight = panelRect.height;
107
+
108
+ const computePosition = (targetSide) => {
109
+ let left = anchorRect.left + offsetX;
110
+ let top = anchorRect.top + offsetY;
111
+
112
+ if (targetSide === "top" || targetSide === "bottom") {
113
+ if (align === "center") left = anchorRect.left + ((anchorRect.width - panelWidth) / 2) + offsetX;
114
+ else if (align === "end") left = anchorRect.right - panelWidth + offsetX;
115
+ top = targetSide === "top"
116
+ ? anchorRect.top - panelHeight - offsetY
117
+ : anchorRect.bottom + offsetY;
118
+ } else {
119
+ if (align === "center") top = anchorRect.top + ((anchorRect.height - panelHeight) / 2) + offsetY;
120
+ else if (align === "end") top = anchorRect.bottom - panelHeight + offsetY;
121
+ left = targetSide === "left"
122
+ ? anchorRect.left - panelWidth - offsetX
123
+ : anchorRect.right + offsetX;
124
+ }
125
+
126
+ return { left, top };
127
+ };
128
+
129
+ const availableSpace = (targetSide) => {
130
+ if (targetSide === "top") return anchorRect.top - offsetY - gutter;
131
+ if (targetSide === "bottom") return viewportHeight - anchorRect.bottom - offsetY - gutter;
132
+ if (targetSide === "left") return anchorRect.left - offsetX - gutter;
133
+ return viewportWidth - anchorRect.right - offsetX - gutter;
134
+ };
135
+
136
+ const preferredSpace = availableSpace(side);
137
+ const oppositeSpace = availableSpace(oppositeSide);
138
+ const requiredSpace = isVertical ? panelHeight : panelWidth;
139
+ const resolvedSide = (requiredSpace > preferredSpace && oppositeSpace > preferredSpace)
140
+ ? oppositeSide
141
+ : side;
142
+ const resolvedPosition = computePosition(resolvedSide);
143
+
144
+ const minLeft = viewportOffsetLeft + gutter;
145
+ const minTop = viewportOffsetTop + gutter;
146
+ const maxLeft = Math.max(minLeft, viewportOffsetLeft + viewportWidth - panelWidth - gutter);
147
+ const maxTop = Math.max(minTop, viewportOffsetTop + viewportHeight - panelHeight - gutter);
148
+ const left = clamp(resolvedPosition.left, minLeft, maxLeft);
149
+ const top = clamp(resolvedPosition.top, minTop, maxTop);
150
+
151
+ panel.style.position = "fixed";
152
+ panel.style.left = `${left}px`;
153
+ panel.style.top = `${top}px`;
154
+ }
155
+
156
+ return {
157
+ ensureRoot,
158
+ focusFirst,
159
+ trapFocus,
160
+ applyAnchoredPosition
161
+ };
162
+ })();