dalila 1.9.3 → 1.9.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Dalila
1
+ # 🐰✂️ Dalila
2
2
 
3
3
  **DOM-first reactivity without the re-renders.**
4
4
 
@@ -58,7 +58,8 @@ bind(document.getElementById('app')!, ctx);
58
58
 
59
59
  ### Runtime
60
60
 
61
- - [Template Binding](./docs/runtime/bind.md) — `bind()`, text interpolation, events
61
+ - [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, text interpolation, events
62
+ - [Components](./docs/runtime/component.md) — `defineComponent`, typed props/emits/refs, slots
62
63
  - [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
63
64
 
64
65
  ### Routing
@@ -107,7 +108,7 @@ Firefox extension workflows:
107
108
 
108
109
  ```
109
110
  dalila → signal, computed, effect, batch, ...
110
- dalila/runtime → bind() for HTML templates
111
+ dalila/runtime → bind(), mount(), configure(), defineComponent()
111
112
  dalila/context → createContext, provide, inject
112
113
  dalila/http → createHttpClient with XSRF protection
113
114
  ```
@@ -130,10 +131,13 @@ count.set(5); // logs: Count is 5
130
131
  ### Template Binding
131
132
 
132
133
  ```ts
133
- import { bind } from 'dalila/runtime';
134
+ import { configure, mount } from 'dalila/runtime';
135
+
136
+ // Global component registry and options
137
+ configure({ components: [MyComponent] });
134
138
 
135
- // Binds {tokens}, d-on-*, d-when, d-match to the DOM
136
- const dispose = bind(rootElement, ctx);
139
+ // Bind a selector to a reactive view-model
140
+ const dispose = mount('.app', { count: signal(0) });
137
141
 
138
142
  // Cleanup when done
139
143
  dispose();
@@ -279,39 +283,46 @@ async function handleSubmit(data, { signal }) {
279
283
 
280
284
  ```ts
281
285
  // page.ts
282
- import { signal } from 'dalila';
286
+ import { computed, signal } from 'dalila';
283
287
  import { createDialog, mountUI } from 'dalila/components/ui';
284
288
 
285
- const dialog = createDialog();
289
+ class HomePageVM {
290
+ count = signal(0);
291
+ status = computed(() => `Count: ${this.count()}`);
292
+ dialog = createDialog({ closeOnBackdrop: true, closeOnEscape: true });
293
+ increment = () => this.count.update(n => n + 1);
294
+ openModal = () => this.dialog.show();
295
+ closeModal = () => this.dialog.close();
296
+ }
286
297
 
287
298
  export function loader() {
288
- const count = signal(0);
289
-
290
- return {
291
- count,
292
- increment: () => count.update(n => n + 1),
293
- openDialog: () => dialog.show(),
294
- };
299
+ return new HomePageVM();
295
300
  }
296
301
 
297
302
  // Called after view is mounted
298
- export function onMount(root: HTMLElement) {
299
- mountUI(root, {
300
- dialogs: { dialog }
303
+ export function onMount(root: HTMLElement, data: HomePageVM) {
304
+ return mountUI(root, {
305
+ dialogs: { dialog: data.dialog },
306
+ events: []
301
307
  });
302
308
  }
309
+
310
+ // Optional extra hook on route leave
311
+ export function onUnmount(_root: HTMLElement) {}
303
312
  ```
304
313
 
305
314
  ```html
306
315
  <!-- page.html -->
307
- <d-button d-on-click="openDialog">Open Dialog</d-button>
316
+ <d-button d-on-click="increment">Increment</d-button>
317
+ <d-button d-on-click="openModal">Open Dialog</d-button>
308
318
 
309
319
  <d-dialog d-ui="dialog">
310
320
  <d-dialog-header>
311
- <d-dialog-title>Count: {count}</d-dialog-title>
321
+ <d-dialog-title>{status}</d-dialog-title>
322
+ <d-dialog-close d-on-click="closeModal">&times;</d-dialog-close>
312
323
  </d-dialog-header>
313
324
  <d-dialog-body>
314
- <d-button d-on-click="increment">Increment</d-button>
325
+ <p>Modal controlled by the official route + UI pattern.</p>
315
326
  </d-dialog-body>
316
327
  </d-dialog>
317
328
  ```
package/dist/cli/check.js CHANGED
@@ -520,7 +520,7 @@ function extractTemplateIdentifiers(html) {
520
520
  i++;
521
521
  }
522
522
  // --- 2. Directive scanning (supports single and double quotes) ---
523
- const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-html|d-attr-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
523
+ const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-html|d-attr-[a-zA-Z][\w-]*|d-bind-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
524
524
  DIRECTIVE_RE.lastIndex = 0;
525
525
  let match;
526
526
  while ((match = DIRECTIVE_RE.exec(html))) {
package/dist/cli/index.js CHANGED
@@ -9,7 +9,7 @@ const routeArgs = args.slice(2);
9
9
  const WATCH_DEBOUNCE_MS = 120;
10
10
  function showHelp() {
11
11
  console.log(`
12
- Dalila CLI
12
+ 🐰✂️ Dalila CLI
13
13
 
14
14
  Usage:
15
15
  dalila routes generate [options] Generate routes + manifest from app file structure
@@ -32,7 +32,7 @@ Examples:
32
32
  }
33
33
  function showRoutesHelp() {
34
34
  console.log(`
35
- Dalila CLI - Routes
35
+ 🐰✂️ Dalila CLI - Routes
36
36
 
37
37
  Usage:
38
38
  dalila routes generate [options] Generate routes + manifest from app file structure
@@ -52,7 +52,7 @@ Examples:
52
52
  }
53
53
  function showCheckHelp() {
54
54
  console.log(`
55
- Dalila CLI - Check
55
+ 🐰✂️ Dalila CLI - Check
56
56
 
57
57
  Usage:
58
58
  dalila check [path] [options] Static analysis of HTML templates
@@ -182,7 +182,7 @@ function resolveGenerateConfig(cliArgs, cwd = process.cwd()) {
182
182
  async function generateRoutes(cliArgs) {
183
183
  const { appDir, outputPath } = resolveGenerateConfig(cliArgs);
184
184
  console.log('');
185
- console.log('🚀 Dalila Routes Generator');
185
+ console.log('🐰✂️ Dalila Routes Generator');
186
186
  console.log('');
187
187
  try {
188
188
  await generateRoutesFile(appDir, outputPath);
@@ -243,7 +243,7 @@ function watchRoutes(cliArgs) {
243
243
  process.exit(1);
244
244
  }
245
245
  console.log('');
246
- console.log('👀 Dalila Routes Watch');
246
+ console.log('🐰✂️ Dalila Routes Watch');
247
247
  console.log(` app: ${appDir}`);
248
248
  console.log(` output: ${outputPath}`);
249
249
  console.log('');
@@ -467,16 +467,31 @@ function pageProps(pageHtml, pageTs) {
467
467
  if (hasNamedExport(pageTs, 'onMount')) {
468
468
  if (pageTs.lazy) {
469
469
  const lazyLoader = `${pageTs.importName}_lazy`;
470
- props.push(`onMount: (root: HTMLElement) => ${lazyLoader}().then(mod => {
470
+ props.push(`onMount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
471
471
  if (typeof (mod as any).onMount === 'function') {
472
- (mod as any).onMount(root);
472
+ return (mod as any).onMount(root, data, ctx);
473
473
  }
474
+ return undefined;
474
475
  })`);
475
476
  }
476
477
  else {
477
478
  props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
478
479
  }
479
480
  }
481
+ if (hasNamedExport(pageTs, 'onUnmount')) {
482
+ if (pageTs.lazy) {
483
+ const lazyLoader = `${pageTs.importName}_lazy`;
484
+ props.push(`onUnmount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
485
+ if (typeof (mod as any).onUnmount === 'function') {
486
+ return (mod as any).onUnmount(root, data, ctx);
487
+ }
488
+ return undefined;
489
+ })`);
490
+ }
491
+ else {
492
+ props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
493
+ }
494
+ }
480
495
  }
481
496
  else {
482
497
  props.push(`view: (ctx) => fromHtml(${viewConst}, { data: ${routeDataExpr()}, scope: ctx.scope })`);
@@ -501,6 +516,12 @@ function pageProps(pageHtml, pageTs) {
501
516
  if (hasNamedExport(pageTs, 'validation')) {
502
517
  props.push(`validation: ${moduleExport(pageTs, 'validation', { allowValue: true })}`);
503
518
  }
519
+ if (hasNamedExport(pageTs, 'onMount')) {
520
+ props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
521
+ }
522
+ if (hasNamedExport(pageTs, 'onUnmount')) {
523
+ props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
524
+ }
504
525
  return props;
505
526
  }
506
527
  function layoutProps(indent, layoutHtml, layoutTs) {
@@ -33,6 +33,9 @@ export type RouteRedirectResult = string | null | undefined | Promise<string | n
33
33
  export type RouteMiddlewareResult = RouteGuardResult;
34
34
  export type RouteMiddleware = (ctx: RouteCtx) => RouteMiddlewareResult;
35
35
  export type RouteMiddlewareResolver = RouteMiddleware[] | ((ctx: RouteCtx) => RouteMiddleware[] | null | undefined | Promise<RouteMiddleware[] | null | undefined>);
36
+ export type RouteMountCleanup = () => void;
37
+ export type RouteMountResult = void | RouteMountCleanup | Promise<void | RouteMountCleanup>;
38
+ export type RouteUnmountResult = void | Promise<void>;
36
39
  /**
37
40
  * Route definition.
38
41
  *
@@ -50,7 +53,8 @@ export interface RouteTable<T = any> {
50
53
  layout?: (ctx: RouteCtx, child: Node | DocumentFragment | Node[], data: T) => Node | DocumentFragment | Node[];
51
54
  loader?: (ctx: RouteCtx) => Promise<T>;
52
55
  preload?: (ctx: RouteCtx) => Promise<T>;
53
- onMount?: (root: HTMLElement) => void;
56
+ onMount?: (root: HTMLElement, data: T, ctx: RouteCtx) => RouteMountResult;
57
+ onUnmount?: (root: HTMLElement, data: T, ctx: RouteCtx) => RouteUnmountResult;
54
58
  pending?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
55
59
  error?: (ctx: RouteCtx, error: unknown, data?: T) => Node | DocumentFragment | Node[];
56
60
  notFound?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
@@ -109,6 +109,11 @@ export function createRouter(config) {
109
109
  let currentLoaderController = null;
110
110
  let started = false;
111
111
  let navigationToken = 0;
112
+ let activeLeafRoute = null;
113
+ let activeRouteMountCleanup = null;
114
+ let activeRouteUnmountHook = null;
115
+ let activeLeafData = undefined;
116
+ let activeLeafCtx = null;
112
117
  const transitionCoalescing = new Map();
113
118
  const scrollPositions = new LRUCache(config.scrollPositionsCacheSize ?? 100);
114
119
  const preloadTagsByKey = new Map();
@@ -582,10 +587,43 @@ export function createRouter(config) {
582
587
  replace: true
583
588
  };
584
589
  }
590
+ function runRouteUnmountLifecycle() {
591
+ const cleanup = activeRouteMountCleanup;
592
+ const onUnmount = activeRouteUnmountHook;
593
+ const data = activeLeafData;
594
+ const ctx = activeLeafCtx;
595
+ activeRouteMountCleanup = null;
596
+ activeRouteUnmountHook = null;
597
+ activeLeafRoute = null;
598
+ activeLeafData = undefined;
599
+ activeLeafCtx = null;
600
+ if (cleanup) {
601
+ try {
602
+ cleanup();
603
+ }
604
+ catch (error) {
605
+ console.error('[Dalila] Error in onMount cleanup lifecycle hook:', error);
606
+ }
607
+ }
608
+ if (onUnmount) {
609
+ try {
610
+ const result = onUnmount(outletElement, data, ctx);
611
+ if (result && typeof result.then === 'function') {
612
+ void result.catch((error) => {
613
+ console.error('[Dalila] Error in onUnmount lifecycle hook:', error);
614
+ });
615
+ }
616
+ }
617
+ catch (error) {
618
+ console.error('[Dalila] Error in onUnmount lifecycle hook:', error);
619
+ }
620
+ }
621
+ }
585
622
  /**
586
623
  * Mount nodes into outlet and remove loading state
587
624
  */
588
625
  function mountToOutlet(...nodes) {
626
+ runRouteUnmountLifecycle();
589
627
  outletElement.replaceChildren(...nodes);
590
628
  queueMicrotask(() => {
591
629
  outletElement.removeAttribute('d-loading');
@@ -950,7 +988,7 @@ export function createRouter(config) {
950
988
  }
951
989
  commitRouteState();
952
990
  try {
953
- mountViewStack(matchStack, ctx, dataStack);
991
+ mountViewStack(matchStack, ctx, dataStack, token);
954
992
  await finishSuccessfulTransition();
955
993
  }
956
994
  catch (error) {
@@ -986,16 +1024,18 @@ export function createRouter(config) {
986
1024
  return promise;
987
1025
  }
988
1026
  /** Compose and mount the view stack (leaf view wrapped by parent layouts). */
989
- function mountViewStack(matchStack, ctx, dataStack) {
1027
+ function mountViewStack(matchStack, ctx, dataStack, navToken) {
990
1028
  try {
991
1029
  let content = null;
992
1030
  let leafRoute = null;
1031
+ let leafData = undefined;
993
1032
  for (let i = matchStack.length - 1; i >= 0; i--) {
994
1033
  const match = matchStack[i];
995
1034
  const data = dataStack[i];
996
1035
  const route = match.route;
997
1036
  if (i === matchStack.length - 1) {
998
1037
  leafRoute = route;
1038
+ leafData = data;
999
1039
  if (!route.view) {
1000
1040
  console.warn(`[Dalila] Leaf route ${match.path} has no view function`);
1001
1041
  return;
@@ -1027,11 +1067,54 @@ export function createRouter(config) {
1027
1067
  if (content) {
1028
1068
  const nodes = Array.isArray(content) ? content : [content];
1029
1069
  mountToOutlet(...nodes);
1070
+ activeLeafRoute = leafRoute ?? null;
1071
+ activeRouteUnmountHook = leafRoute?.onUnmount ?? null;
1072
+ activeLeafData = leafData;
1073
+ activeLeafCtx = ctx;
1030
1074
  // Call onMount lifecycle hook if present
1031
1075
  if (leafRoute?.onMount) {
1032
1076
  queueMicrotask(() => {
1077
+ if (navigationToken !== navToken)
1078
+ return;
1079
+ if (activeLeafRoute !== leafRoute)
1080
+ return;
1033
1081
  try {
1034
- leafRoute.onMount(outletElement);
1082
+ const result = leafRoute.onMount(outletElement, leafData, ctx);
1083
+ if (typeof result === 'function') {
1084
+ if (navigationToken === navToken && activeLeafRoute === leafRoute) {
1085
+ activeRouteMountCleanup = result;
1086
+ }
1087
+ else {
1088
+ try {
1089
+ result();
1090
+ }
1091
+ catch (cleanupError) {
1092
+ console.error('[Dalila] Error in stale onMount cleanup lifecycle hook:', cleanupError);
1093
+ }
1094
+ }
1095
+ return;
1096
+ }
1097
+ if (result && typeof result.then === 'function') {
1098
+ void result
1099
+ .then((resolved) => {
1100
+ if (typeof resolved !== 'function')
1101
+ return;
1102
+ if (navigationToken === navToken && activeLeafRoute === leafRoute) {
1103
+ activeRouteMountCleanup = resolved;
1104
+ }
1105
+ else {
1106
+ try {
1107
+ resolved();
1108
+ }
1109
+ catch (cleanupError) {
1110
+ console.error('[Dalila] Error in stale async onMount cleanup lifecycle hook:', cleanupError);
1111
+ }
1112
+ }
1113
+ })
1114
+ .catch((error) => {
1115
+ console.error('[Dalila] Error in onMount lifecycle hook:', error);
1116
+ });
1117
+ }
1035
1118
  }
1036
1119
  catch (error) {
1037
1120
  console.error('[Dalila] Error in onMount lifecycle hook:', error);
@@ -1413,6 +1496,7 @@ export function createRouter(config) {
1413
1496
  document.removeEventListener('click', handleLink);
1414
1497
  document.removeEventListener('pointerover', handleLinkIntent);
1415
1498
  document.removeEventListener('focusin', handleLinkIntent);
1499
+ runRouteUnmountLifecycle();
1416
1500
  if (currentLoaderController) {
1417
1501
  currentLoaderController.abort();
1418
1502
  currentLoaderController = null;
@@ -0,0 +1,2 @@
1
+ import type { RouteTable } from 'dalila/router';
2
+ export declare const routes: RouteTable[];
@@ -0,0 +1,76 @@
1
+ // This file is auto-generated by 'dalila routes generate'
2
+ // Do not edit manually - your changes will be overwritten
3
+ import { fromHtml } from 'dalila';
4
+ const layout_html = `<div class="app-shell">
5
+ <h1>Router + PageVM + Componente</h1>
6
+ <nav>
7
+ <a href="/examples/router-simple/" d-link>Home</a>
8
+ <a href="/examples/router-simple/about" d-link>Sobre</a>
9
+ </nav>
10
+ <main id="outlet-slot" data-slot="children"></main>
11
+ </div>
12
+ `;
13
+ const page_html = `<section>
14
+ <h2>Home</h2>
15
+ <p>PageVM da rota: <strong>HomePageVM</strong></p>
16
+ <status-badge d-props-text="status"></status-badge>
17
+ <div style="display:flex; gap:.5rem; margin-top:.75rem;">
18
+ <button d-on-click="increment">Incrementar</button>
19
+ <button d-on-click="openModal">Abrir modal</button>
20
+ </div>
21
+
22
+ <d-dialog d-ui="infoDialog" class="d-dialog">
23
+ <d-dialog-header>
24
+ <d-dialog-title>Modal Dalila</d-dialog-title>
25
+ <d-dialog-close d-on-click="closeModal">&times;</d-dialog-close>
26
+ </d-dialog-header>
27
+ <d-dialog-body>
28
+ <p>This modal is mounted with <code>mountUI()</code> in the route <code>onMount</code>.</p>
29
+ <p>Status atual: <strong d-text="status"></strong></p>
30
+ </d-dialog-body>
31
+ <d-dialog-footer>
32
+ <button d-on-click="closeModal">Fechar</button>
33
+ </d-dialog-footer>
34
+ </d-dialog>
35
+ </section>
36
+ `;
37
+ const about_page_html = `<section>
38
+ <h2>Sobre</h2>
39
+ <p>PageVM da rota: <strong>AboutPageVM</strong></p>
40
+ <p>Nome atual: <strong d-text="nomeUpper"></strong></p>
41
+ <input d-bind-value="nome" placeholder="Digite um nome" />
42
+ </section>
43
+ `;
44
+ const page_lazy = () => import('./app/page.js');
45
+ const about_page_lazy = () => import('./app/about/page.js');
46
+ export const routes = [
47
+ {
48
+ path: '/',
49
+ layout: (ctx, children) => fromHtml(layout_html, { data: { ...ctx.params, params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, children, scope: ctx.scope }),
50
+ children: [
51
+ { path: '', view: (ctx, data) => fromHtml(page_html, { data: { ...ctx.params, ...(data ?? {}), params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, scope: ctx.scope }), loader: (...args) => page_lazy().then(mod => {
52
+ const exported = mod.loader;
53
+ if (typeof exported === 'function') {
54
+ return exported(...args);
55
+ }
56
+ return undefined;
57
+ }), onMount: (root, data, ctx) => page_lazy().then(mod => {
58
+ if (typeof mod.onMount === 'function') {
59
+ return mod.onMount(root, data, ctx);
60
+ }
61
+ return undefined;
62
+ }) },
63
+ {
64
+ path: 'about',
65
+ view: (ctx, data) => fromHtml(about_page_html, { data: { ...ctx.params, ...(data ?? {}), params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, scope: ctx.scope }),
66
+ loader: (...args) => about_page_lazy().then(mod => {
67
+ const exported = mod.loader;
68
+ if (typeof exported === 'function') {
69
+ return exported(...args);
70
+ }
71
+ return undefined;
72
+ }),
73
+ }
74
+ ]
75
+ }
76
+ ];
@@ -0,0 +1,4 @@
1
+ import type { RouteManifestEntry } from 'dalila/router';
2
+ export declare const routeManifest: RouteManifestEntry[];
3
+ export declare function getRouteManifestEntry(id: string): RouteManifestEntry | undefined;
4
+ export declare function prefetchRouteById(id: string): Promise<void>;
@@ -0,0 +1,32 @@
1
+ // This file is auto-generated by 'dalila routes generate'
2
+ // Do not edit manually - your changes will be overwritten
3
+ export const routeManifest = [
4
+ {
5
+ id: 'root',
6
+ pattern: '/',
7
+ score: 1000,
8
+ paramKeys: [],
9
+ tags: [],
10
+ modules: ["./app/page.js"],
11
+ load: () => import('./app/page.js').then(() => undefined)
12
+ },
13
+ {
14
+ id: 'about',
15
+ pattern: '/about',
16
+ score: 301,
17
+ paramKeys: [],
18
+ tags: [],
19
+ modules: ["./app/page.js", "./app/about/page.js"],
20
+ load: () => Promise.all([import('./app/page.js'), import('./app/about/page.js')]).then(() => undefined)
21
+ },
22
+ ];
23
+ const manifestById = new Map(routeManifest.map(route => [route.id, route]));
24
+ export function getRouteManifestEntry(id) {
25
+ return manifestById.get(id);
26
+ }
27
+ export async function prefetchRouteById(id) {
28
+ const entry = manifestById.get(id);
29
+ if (!entry)
30
+ return;
31
+ await entry.load();
32
+ }
@@ -0,0 +1,11 @@
1
+ export type RoutePattern = '/' | '/about';
2
+ export type RouteParamsByPattern = {
3
+ '/': {};
4
+ '/about': {};
5
+ };
6
+ export type RouteSearchByPattern = {
7
+ [P in RoutePattern]: Record<string, string | string[]>;
8
+ };
9
+ export type RouteParams<P extends RoutePattern> = RouteParamsByPattern[P];
10
+ export type RouteSearch<P extends RoutePattern> = RouteSearchByPattern[P];
11
+ export declare function buildRoutePath<P extends RoutePattern>(pattern: P, params: RouteParams<P>): string;
@@ -0,0 +1,37 @@
1
+ // This file is auto-generated by 'dalila routes generate'
2
+ // Do not edit manually - your changes will be overwritten
3
+ export function buildRoutePath(pattern, params) {
4
+ const out = [];
5
+ for (const segment of pattern.split('/').filter(Boolean)) {
6
+ if (!segment.startsWith(':')) {
7
+ out.push(segment);
8
+ continue;
9
+ }
10
+ const isOptionalCatchAll = segment.endsWith('*?');
11
+ const isCatchAll = isOptionalCatchAll || segment.endsWith('*');
12
+ const key = segment.slice(1, isCatchAll ? (isOptionalCatchAll ? -2 : -1) : undefined);
13
+ const value = params[key];
14
+ if (isCatchAll) {
15
+ if (value === undefined || value === null) {
16
+ if (isOptionalCatchAll)
17
+ continue;
18
+ throw new Error(`Missing route param: ${key}`);
19
+ }
20
+ if (!Array.isArray(value)) {
21
+ throw new Error(`Route param "${key}" must be an array`);
22
+ }
23
+ if (value.length === 0) {
24
+ if (isOptionalCatchAll)
25
+ continue;
26
+ throw new Error(`Route param "${key}" cannot be empty`);
27
+ }
28
+ out.push(...value.map(v => encodeURIComponent(String(v))));
29
+ continue;
30
+ }
31
+ if (value === undefined || value === null) {
32
+ throw new Error(`Missing route param: ${key}`);
33
+ }
34
+ out.push(encodeURIComponent(String(value)));
35
+ }
36
+ return out.length === 0 ? '/' : `/${out.join('/')}`;
37
+ }
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @module dalila/runtime
8
8
  */
9
+ import type { Component } from './component.js';
9
10
  export interface BindOptions {
10
11
  /**
11
12
  * Event types to bind (default: click, input, change, submit, keydown, keyup)
@@ -25,6 +26,10 @@ export interface BindOptions {
25
26
  /** Time-to-live (ms) per plan, refreshed on hit (0 disables cache). */
26
27
  ttlMs?: number;
27
28
  };
29
+ /** Component registry — accepts map `{ tag: component }` or array `[component]` */
30
+ components?: Record<string, Component> | Component[];
31
+ /** Error policy for component `ctx.onMount()` callbacks. Default: 'log'. */
32
+ onMountError?: 'log' | 'throw';
28
33
  /**
29
34
  * Internal flag — set by fromHtml for router/template rendering.
30
35
  * Skips HMR context registration but KEEPS d-ready/d-loading lifecycle.
@@ -54,6 +59,23 @@ export interface BindHandle {
54
59
  getRef(name: string): Element | null;
55
60
  getRefs(): Readonly<Record<string, Element>>;
56
61
  }
62
+ /**
63
+ * Set global defaults for all `bind()` / `mount()` calls.
64
+ *
65
+ * Options set here are merged with per-call options (per-call wins).
66
+ * Call with an empty object to reset.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import { configure } from 'dalila/runtime';
71
+ *
72
+ * configure({
73
+ * components: [FruitPicker],
74
+ * onMountError: 'log',
75
+ * });
76
+ * ```
77
+ */
78
+ export declare function configure(config: BindOptions): void;
57
79
  /**
58
80
  * Bind a DOM tree to a reactive context.
59
81
  *
@@ -77,7 +99,7 @@ export interface BindHandle {
77
99
  * dispose();
78
100
  * ```
79
101
  */
80
- export declare function bind<T extends Record<string, unknown> = BindContext>(root: Element, ctx: T, options?: BindOptions): BindHandle;
102
+ export declare function bind<T extends Record<string, unknown> = BindContext>(root: Element | string, ctx: T, options?: BindOptions): BindHandle;
81
103
  /**
82
104
  * Automatically bind when DOM is ready.
83
105
  * Useful for simple pages without a build step.
@@ -91,3 +113,14 @@ export declare function bind<T extends Record<string, unknown> = BindContext>(ro
91
113
  * ```
92
114
  */
93
115
  export declare function autoBind<T extends Record<string, unknown> = BindContext>(selector: string, ctx: T, options?: BindOptions): Promise<BindHandle>;
116
+ /**
117
+ * Mount a component imperatively, or bind a selector to a view-model.
118
+ *
119
+ * Overload 1 — `mount(selector, vm, options?)`:
120
+ * Shorthand for `bind(selector, vm, options)`.
121
+ *
122
+ * Overload 2 — `mount(component, target, props?)`:
123
+ * Mount a component created with defineComponent() into a target element.
124
+ */
125
+ export declare function mount<T extends object>(selector: string, vm: T, options?: BindOptions): BindHandle;
126
+ export declare function mount(component: Component, target: Element, props?: Record<string, unknown>): BindHandle;