@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,399 @@
1
+ // -- ROUTER --
2
+ JSswift.router = (() => {
3
+ let mode = "history"; // "history" | "hash" | "auto"
4
+ let routes = [];
5
+ let outlet = null;
6
+ let unmountCurrent = null;
7
+ let base = "";
8
+ let beforeHook = null;
9
+
10
+ // --- Router DevTools state ---
11
+ let _currentCtx = null;
12
+ let _history = [];
13
+ let _tracing = false;
14
+ const {
15
+ normalizePath,
16
+ stripBase: stripBasePath,
17
+ compilePattern,
18
+ parseQuery,
19
+ matchRoute,
20
+ pushHistoryEntry
21
+ } = JSswift._routerShared;
22
+
23
+ const meta = {
24
+ setOutlet: { description: "imposta il contenitore del router" },
25
+ setBase: { description: "imposta il base path del router" },
26
+ add: { description: "per aggiungere la route" },
27
+ notFound: { description: "imposta la view 404" },
28
+ beforeEach: { description: "hook prima di ogni navigazione" },
29
+ start: { description: "avvia il router" },
30
+ navigate: { description: "naviga verso una route" },
31
+ subscribe: { description: "sottoscrive agli aggiornamenti di route" },
32
+ current: { description: "ritorna l'URL corrente" },
33
+ isActive: { description: "verifica se una route è attiva" },
34
+ status: { description: "stringa di stato router" },
35
+ inspect: { description: "stampa info diagnostiche" },
36
+ trace: { description: "abilita/disabilita tracing" },
37
+ history: { description: "ritorna la history interna" },
38
+ setURLOnly: { description: "aggiorna solo l'URL del browser senza render/meta" }
39
+ };
40
+
41
+ function setURLOnly(urlLike, { replace = false } = {}) {
42
+ const url = typeof urlLike === "string" ? new URL(urlLike, window.location.origin) : urlLike;
43
+ const fullPath = url.pathname + url.search + url.hash;
44
+
45
+ if (mode === "hash" || mode === "auto") {
46
+ if (history.pushState) {
47
+ const h = "#" + fullPath;
48
+ if (replace) history.replaceState({}, "", h);
49
+ else history.pushState({}, "", h);
50
+ return;
51
+ }
52
+ const h = "#" + fullPath;
53
+ if (replace) window.location.replace(h);
54
+ else window.location.hash = h;
55
+ return;
56
+ }
57
+
58
+ if (replace) history.replaceState({}, "", base + fullPath);
59
+ else history.pushState({}, "", base + fullPath);
60
+ }
61
+
62
+
63
+ function getCurrentURL() {
64
+ if (mode === "hash") {
65
+ const h = window.location.hash || "#/";
66
+ const url = h.startsWith("#") ? h.slice(1) : h;
67
+ return new URL(url, window.location.origin);
68
+ }
69
+
70
+ // auto
71
+ if (mode === "auto") {
72
+ if (window.location.hash.startsWith("#/")) {
73
+ const url = window.location.hash.slice(1);
74
+ return new URL(url, window.location.origin);
75
+ }
76
+ }
77
+
78
+ return new URL(window.location.href);
79
+ }
80
+
81
+ function updateURL(url, replace = false) {
82
+ if (mode === "hash" || (mode === "auto" && !history.pushState)) {
83
+ const h = "#" + (url.pathname + url.search + url.hash);
84
+ if (replace) {
85
+ window.location.replace(h);
86
+ } else {
87
+ window.location.hash = h;
88
+ }
89
+ return;
90
+ }
91
+
92
+ if (replace) history.replaceState({}, "", base + url.pathname + url.search + url.hash);
93
+ else history.pushState({}, "", base + url.pathname + url.search + url.hash);
94
+ }
95
+
96
+ function setMode(m) {
97
+ mode = m || "history";
98
+ }
99
+
100
+ function stripBase(path) {
101
+ return stripBasePath(path, base);
102
+ }
103
+
104
+ function isActive(path) {
105
+ if (!path) return false;
106
+ const currentPath = _currentCtx ? _currentCtx.path : stripBase(getCurrentURL().pathname);
107
+ let pathname = path;
108
+
109
+ if (typeof pathname === "string" && (pathname.includes("?") || pathname.includes("#") || pathname.includes("://"))) {
110
+ pathname = new URL(pathname, window.location.origin).pathname;
111
+ }
112
+
113
+ const target = stripBase(pathname);
114
+ if (!target.includes(":")) return target === currentPath;
115
+ const compiled = compilePattern(target);
116
+ return compiled.regex.test(currentPath);
117
+ }
118
+
119
+ async function render(urlLike, { replace = false } = {}) {
120
+ if (!outlet) {
121
+ console.warn("[router] outlet non impostato. Usa JSswift.router.setOutlet('#app').");
122
+ return;
123
+ }
124
+
125
+ const url = typeof urlLike === "string" ? new URL(urlLike, window.location.origin) : urlLike;
126
+ if (_tracing) {
127
+ console.log("[router.navigate]", url.toString());
128
+ }
129
+ const fullPath = url.pathname + url.search + url.hash;
130
+
131
+ // aggiorna address bar
132
+ updateURL(url, replace);
133
+
134
+ const pathname = stripBase(url.pathname);
135
+ const m = matchRoute(pathname, routes);
136
+
137
+ const ctx = {
138
+ path: pathname,
139
+ fullPath,
140
+ params: m ? m.params : {},
141
+ query: parseQuery(url.search),
142
+ hash: url.hash || "",
143
+ navigate, // per navigare dentro view
144
+ outlet
145
+ };
146
+
147
+ // beforeEach (redirect/block)
148
+ if (beforeHook) {
149
+ const res = await beforeHook(ctx);
150
+ if (res === false) return; // blocca
151
+ if (typeof res === "string") { // redirect
152
+ navigate(res, { replace: true });
153
+ return;
154
+ }
155
+ }
156
+
157
+ // unmount view precedente
158
+ if (typeof unmountCurrent === "function") {
159
+ try { unmountCurrent(); } catch (e) { console.error("[router] unmount error:", e); }
160
+ unmountCurrent = null;
161
+ }
162
+
163
+ // 404
164
+ if (!m) {
165
+ const view404 = routes._notFound;
166
+ if (view404) {
167
+ unmountCurrent = JSswift.mount(outlet, () => view404(ctx), { clear: true });
168
+ } else {
169
+ unmountCurrent = JSswift.mount(outlet, _.div("404"), { clear: true });
170
+ }
171
+ notifyRoute(ctx);
172
+ _currentCtx = ctx;
173
+ pushHistoryEntry(_history, ctx);
174
+ return;
175
+ }
176
+
177
+ // mount view matchata
178
+ // mount view matchata (supporta nested)
179
+ const view = m.route.view;
180
+ const parent = m.parent; // se esiste, questa route è child
181
+ let childMounted = null;
182
+
183
+ ctx.renderChild = (target) => {
184
+ if (!ctx._child) return null;
185
+
186
+ const childOutlet = typeof target === "string" ? ctx.outlet.querySelector(target) : target;
187
+ if (!childOutlet) {
188
+ console.warn("[router] child outlet non trovato:", target);
189
+ return null;
190
+ }
191
+
192
+ // monta child (clear true)
193
+ childMounted = JSswift.mount(childOutlet, () => ctx._child.view(ctx._child.ctx), { clear: true });
194
+ return childMounted;
195
+ };
196
+
197
+ if (parent) {
198
+ // nested: render layout (parent.view) e prepara child
199
+ const childRoute = m.route;
200
+
201
+ // ctx child separato (stessi query/hash, params merged)
202
+ const childCtx = {
203
+ ...ctx,
204
+ params: { ...ctx.params }, // include già i params del child match (per come matcha)
205
+ };
206
+
207
+ ctx._child = {
208
+ view: childRoute.view,
209
+ ctx: childCtx
210
+ };
211
+
212
+ // mount layout sul root outlet
213
+ unmountCurrent = JSswift.mount(outlet, () => parent.view(ctx), { clear: true });
214
+ } else {
215
+ // non nested
216
+ ctx._child = null;
217
+ unmountCurrent = JSswift.mount(outlet, () => view(ctx), { clear: true });
218
+ }
219
+
220
+ notifyRoute(ctx);
221
+ // devtools state update
222
+ _currentCtx = ctx;
223
+ pushHistoryEntry(_history, ctx);
224
+
225
+ }
226
+
227
+ function navigate(to, opts = {}) {
228
+ const url = new URL(to, window.location.origin);
229
+ render(url, opts);
230
+ }
231
+
232
+ function start() {
233
+ // back/forward
234
+ window.addEventListener("popstate", () => {
235
+ render(getCurrentURL(), { replace: true });
236
+ });
237
+
238
+ window.addEventListener("hashchange", () => {
239
+ if (mode === "hash" || mode === "auto") {
240
+ render(getCurrentURL(), { replace: true });
241
+ }
242
+ });
243
+
244
+
245
+
246
+ // click interception su <a>
247
+ document.addEventListener("click", (e) => {
248
+ const a = e.target?.closest?.("a");
249
+ if (!a) return;
250
+
251
+ // rispetta:
252
+ // - new tab / middle click / ctrl/cmd
253
+ if (e.defaultPrevented) return;
254
+ if (e.button !== 0) return;
255
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
256
+
257
+ // rispetta download/target
258
+ if (a.hasAttribute("download")) return;
259
+ if (a.target && a.target !== "_self") return;
260
+
261
+ const href = a.getAttribute("href");
262
+ if (!href || href.startsWith("mailto:") || href.startsWith("tel:")) return;
263
+
264
+ // esterno
265
+ const url = new URL(href, window.location.origin);
266
+ if (url.origin !== window.location.origin) return;
267
+
268
+ // hash-only (stessa pagina)
269
+ // se vuoi gestirlo, puoi farlo qui; per ora: lasciamo default se non cambia path
270
+ const current = new URL(window.location.href);
271
+ const samePath = (url.pathname + url.search) === (current.pathname + current.search);
272
+ if (samePath && url.hash) return;
273
+
274
+ // prevent e navigate
275
+ e.preventDefault();
276
+ navigate(url.pathname + url.search + url.hash);
277
+ });
278
+
279
+ // render iniziale
280
+ render(getCurrentURL(), { replace: true });
281
+ }
282
+
283
+ function setOutlet(target) {
284
+ outlet = typeof target === "string" ? JSswift.dom.q(target) : target;
285
+ return outlet;
286
+ }
287
+
288
+ function setBase(b) {
289
+ base = b ? normalizePath(b) : "";
290
+ // base dovrebbe essere "" oppure "/qualcosa"
291
+ if (base === "/") base = "";
292
+ }
293
+
294
+ function add(path, view, children = null) {
295
+ const compiled = compilePattern(path);
296
+ const record = { path: compiled.pattern, view, _compiled: compiled, children: [] };
297
+
298
+ if (Array.isArray(children)) {
299
+ // children: array di [subPath, subView] o [subPath, subView, subChildren]
300
+ for (const c of children) {
301
+ const [subPath, subView, subChildren] = c;
302
+ // child path relativo al parent
303
+ const full = normalizePath(compiled.pattern + "/" + (subPath || "").replace(/^\//, ""));
304
+ record.children.push({
305
+ path: full,
306
+ view: subView,
307
+ _compiled: compilePattern(full),
308
+ children: Array.isArray(subChildren) ? subChildren : []
309
+ });
310
+ }
311
+ }
312
+
313
+ routes.push(record);
314
+ }
315
+
316
+
317
+ function notFound(view) {
318
+ routes._notFound = view;
319
+ }
320
+
321
+ function beforeEach(fn) {
322
+ beforeHook = fn;
323
+ }
324
+
325
+ // --- Router reactive state ---
326
+ let _routeListeners = new Set();
327
+
328
+ function notifyRoute(ctx) {
329
+ for (const fn of _routeListeners) {
330
+ try { fn(ctx); } catch (e) { console.error("[router] listener error:", e); }
331
+ }
332
+ }
333
+
334
+
335
+ // ===============================
336
+ // Router DevTools
337
+ // ===============================
338
+ function routeStatus() {
339
+ if (!_currentCtx) return "ROUTER ⏳ not ready";
340
+ const q = Object.keys(_currentCtx.query || {}).length ? "?" : "";
341
+ return `ROUTER ${_currentCtx.path}${q} ${_currentCtx.hash || ""
342
+ }`;
343
+ }
344
+
345
+ function routeInspect(label = "router") {
346
+ if (!_currentCtx) {
347
+ console.warn("[router.inspect] nessuna route attiva");
348
+ return null;
349
+ }
350
+
351
+ const info = {
352
+ label,
353
+ mode: typeof mode !== "undefined" ? mode : "unknown",
354
+ base,
355
+ path: _currentCtx.path,
356
+ params: _currentCtx.params,
357
+ query: _currentCtx.query,
358
+ hash: _currentCtx.hash,
359
+ outlet,
360
+ timestamp: new Date().toISOString()
361
+ };
362
+
363
+ console.groupCollapsed(`[JSswift.router.inspect] ${label}`);
364
+ console.log(info);
365
+ console.log("history:", _history.slice());
366
+ console.groupEnd();
367
+
368
+ return info;
369
+ }
370
+
371
+ function routeTrace(on = true) {
372
+ _tracing = !!on;
373
+ console.log("[JSswift.router.trace]", _tracing ? "ON" : "OFF");
374
+ }
375
+
376
+ function routeHistory() {
377
+ return _history.slice();
378
+ }
379
+
380
+
381
+ return {
382
+ meta,
383
+ setOutlet, setBase, add, setURLOnly, notFound, beforeEach, start, navigate, isActive, subscribe(fn) {
384
+ _routeListeners.add(fn);
385
+ return () => _routeListeners.delete(fn);
386
+ },
387
+ current() {
388
+ return getCurrentURL ? getCurrentURL() : new URL(window.location.href);
389
+ },
390
+ status: routeStatus,
391
+ inspect: routeInspect,
392
+ trace: routeTrace,
393
+ history: routeHistory
394
+ };
395
+ })();
396
+
397
+ if (JSswift.config?.debug) {
398
+ window.$router = JSswift.router;
399
+ }
@@ -0,0 +1,142 @@
1
+ // alias per compatibilità
2
+ JSswift.signal = JSswift.reactive.signal;
3
+ JSswift.effect = JSswift.reactive.effect;
4
+ JSswift.computed = JSswift.reactive.computed;
5
+ JSswift.untracked = JSswift.reactive.untracked;
6
+ JSswift.batch = JSswift.reactive.batch;
7
+
8
+ function normalizeThemeName(theme) {
9
+ if (theme == null) return null;
10
+ const value = String(theme).trim();
11
+ return value || null;
12
+ }
13
+
14
+ function normalizeThemeList(input) {
15
+ const values = Array.isArray(input)
16
+ ? input
17
+ : typeof input === "string"
18
+ ? input.split(",")
19
+ : [];
20
+ const out = [];
21
+ const seen = new Set();
22
+ for (const item of values) {
23
+ const value = normalizeThemeName(item);
24
+ if (!value || seen.has(value)) continue;
25
+ seen.add(value);
26
+ out.push(value);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ function getThemeRoot() {
32
+ if (typeof document !== "undefined" && document.documentElement) {
33
+ return document.documentElement;
34
+ }
35
+ return JSswift.dom?.q ? JSswift.dom.q("html") : null;
36
+ }
37
+
38
+ function getThemeStorage() {
39
+ try {
40
+ return typeof globalThis !== "undefined" ? globalThis.localStorage || null : null;
41
+ } catch (_error) {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function getThemeStorageKey() {
47
+ return normalizeThemeName(JSswift.theme?.storageKey) || "jsswift:theme";
48
+ }
49
+
50
+ function readSavedTheme() {
51
+ const storage = getThemeStorage();
52
+ if (!storage) return null;
53
+ try {
54
+ return normalizeThemeName(storage.getItem(getThemeStorageKey()));
55
+ } catch (_error) {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function writeSavedTheme(theme) {
61
+ const storage = getThemeStorage();
62
+ if (!storage) return;
63
+ try {
64
+ if (theme == null) {
65
+ storage.removeItem(getThemeStorageKey());
66
+ } else {
67
+ storage.setItem(getThemeStorageKey(), theme);
68
+ }
69
+ } catch (_error) { }
70
+ }
71
+
72
+ function resolveThemeList(themes) {
73
+ const html = getThemeRoot();
74
+ const candidates = [
75
+ themes,
76
+ JSswift.theme?.themes,
77
+ JSswift.config?.themes,
78
+ typeof globalThis !== "undefined" ? globalThis.JSswift_setting?.themes : null,
79
+ typeof globalThis !== "undefined" ? globalThis.JSswift_setting?.themeList : null,
80
+ html?.getAttribute?.("data-themes"),
81
+ ];
82
+
83
+ for (const candidate of candidates) {
84
+ const list = normalizeThemeList(candidate);
85
+ if (list.length) return list;
86
+ }
87
+ return [];
88
+ }
89
+
90
+ JSswift.theme = JSswift.theme || {};
91
+ JSswift.theme.storageKey = getThemeStorageKey();
92
+ if (!normalizeThemeList(JSswift.theme.themes).length) {
93
+ JSswift.theme.themes = resolveThemeList();
94
+ }
95
+
96
+ JSswift.setTheme = function (theme, opts = {}) {
97
+ const value = normalizeThemeName(theme);
98
+ const html = getThemeRoot();
99
+ if (html) {
100
+ if (value == null) html.removeAttribute("data-theme");
101
+ else html.setAttribute("data-theme", value);
102
+ }
103
+ if (opts.persist !== false) writeSavedTheme(value);
104
+ return html;
105
+ };
106
+
107
+ JSswift.getTheme = function (opts = {}) {
108
+ const html = getThemeRoot();
109
+ const current = normalizeThemeName(html?.getAttribute?.("data-theme"));
110
+ if (current) return current;
111
+
112
+ if (opts.storage === false) return null;
113
+
114
+ const saved = readSavedTheme();
115
+ if (saved && html && opts.sync !== false) {
116
+ html.setAttribute("data-theme", saved);
117
+ }
118
+ return saved;
119
+ };
120
+
121
+ JSswift.toggleTheme = function (themes, opts = {}) {
122
+ const list = resolveThemeList(themes);
123
+ const activeList = list.length
124
+ ? list
125
+ : normalizeThemeList([JSswift.getTheme({ sync: false }), "light", "dark"]);
126
+ const current = JSswift.getTheme({ sync: false });
127
+ const fallback = normalizeThemeName(opts.fallback) || activeList[0] || "light";
128
+ const currentIndex = current ? activeList.indexOf(current) : -1;
129
+ const nextTheme = currentIndex >= 0
130
+ ? activeList[(currentIndex + 1) % activeList.length]
131
+ : fallback;
132
+
133
+ JSswift.theme.themes = activeList.slice();
134
+ JSswift.setTheme(nextTheme, opts);
135
+ return nextTheme;
136
+ };
137
+
138
+ const bootTheme = readSavedTheme();
139
+ if (bootTheme) {
140
+ JSswift.setTheme(bootTheme, { persist: false });
141
+ }
142
+ })();
package/src/README.md ADDED
@@ -0,0 +1,58 @@
1
+ ## CMS Source Modules
2
+
3
+ This folder contains the internal source modules of the JSswift core.
4
+
5
+ Manifest:
6
+
7
+ - `modules.json`
8
+
9
+ Current build order:
10
+
11
+ - `00-bootstrap.js`
12
+ - `09-overlay-shared.js`
13
+ - `10-overlay.js`
14
+ - `15-dom-props.js`
15
+ - `16-renderer-shared.js`
16
+ - `17-renderer-children.js`
17
+ - `18-renderer-args.js`
18
+ - `20-renderer.js`
19
+ - `30-rod-core.js`
20
+ - `31-rod-model.js`
21
+ - `32-lifecycle-helpers.js`
22
+ - `32-lifecycle.js`
23
+ - `33-debug-perf.js`
24
+ - `34-rod-devtools.js`
25
+ - `39-store-shared.js`
26
+ - `40-store-core.js`
27
+ - `41-store-extensions.js`
28
+ - `42-plugins.js`
29
+ - `42a-auth-shared.js`
30
+ - `43-auth.js`
31
+ - `43a-http-shared.js`
32
+ - `44-http.js`
33
+ - `45-route-hooks.js`
34
+ - `49-router-shared.js`
35
+ - `49a-ui-meta-shared.js`
36
+ - `50-ui-meta.js`
37
+ - `51-can.js`
38
+ - `52-router.js`
39
+ - `53-footer.js`
40
+
41
+ Build command:
42
+
43
+ ```bash
44
+ npm run build:cms
45
+ ```
46
+
47
+ Generated outputs:
48
+
49
+ - `packages/core/dist/cms.js`
50
+ - `packages/core/dist/min-cms.js`
51
+ - compatibility mirror in `pages/_jsswift-fe/js/`
52
+
53
+ Rules:
54
+
55
+ - edit files in `packages/core/src/`
56
+ - rebuild the core package
57
+ - run `npm test`
58
+ - update `docs/reference/core.md` if the technical contract changes
@@ -0,0 +1,31 @@
1
+ [
2
+ "00-bootstrap.js",
3
+ "09-overlay-shared.js",
4
+ "10-overlay.js",
5
+ "15-dom-props.js",
6
+ "16-renderer-shared.js",
7
+ "17-renderer-children.js",
8
+ "18-renderer-args.js",
9
+ "20-renderer.js",
10
+ "30-rod-core.js",
11
+ "31-rod-model.js",
12
+ "32-lifecycle-helpers.js",
13
+ "32-lifecycle.js",
14
+ "33-debug-perf.js",
15
+ "34-rod-devtools.js",
16
+ "39-store-shared.js",
17
+ "40-store-core.js",
18
+ "41-store-extensions.js",
19
+ "42-plugins.js",
20
+ "42a-auth-shared.js",
21
+ "43-auth.js",
22
+ "43a-http-shared.js",
23
+ "44-http.js",
24
+ "45-route-hooks.js",
25
+ "49-router-shared.js",
26
+ "49a-ui-meta-shared.js",
27
+ "50-ui-meta.js",
28
+ "51-can.js",
29
+ "52-router.js",
30
+ "53-footer.js"
31
+ ]