dalila 1.9.17 → 1.9.19

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
@@ -15,12 +15,14 @@ npm run dev
15
15
 
16
16
  Open http://localhost:4242 to see your app.
17
17
 
18
- ## Manual Installation
18
+ ## Install
19
19
 
20
20
  ```bash
21
21
  npm install dalila
22
22
  ```
23
23
 
24
+ ## Minimal Example
25
+
24
26
  ```html
25
27
  <div id="app">
26
28
  <p>Count: {count}</p>
@@ -44,313 +46,61 @@ bind(document.getElementById('app')!, ctx);
44
46
 
45
47
  ## Docs
46
48
 
47
- ### Getting Started
49
+ ### Start here
48
50
 
49
- - [Overview](./docs/index.md) — Philosophy and quick start
50
- - [Template Spec](./docs/template-spec.md) — Binding syntax reference
51
+ - [Overview](./docs/index.md)
52
+ - [Template Spec](./docs/template-spec.md)
53
+ - [Router](./docs/router.md)
54
+ - [Forms](./docs/forms.md)
55
+ - [UI Components](./docs/ui.md)
56
+ - [HTTP Client](./docs/http.md)
51
57
 
52
58
  ### Core
53
59
 
54
- - [Signals](./docs/core/signals.md) — `signal`, `computed`, `effect`
55
- - [Scopes](./docs/core/scope.md) — Lifecycle management and cleanup
56
- - [Persist](./docs/core/persist.md) — Automatic storage sync for signals
57
- - [Context](./docs/context.md) — Dependency injection
60
+ - [Signals](./docs/core/signals.md)
61
+ - [Scopes](./docs/core/scope.md)
62
+ - [Persist](./docs/core/persist.md)
63
+ - [Context](./docs/context.md)
64
+ - [Scheduler](./docs/core/scheduler.md)
65
+ - [Keys](./docs/core/key.md)
66
+ - [Dev Mode](./docs/core/dev.md)
58
67
 
59
68
  ### Runtime
60
69
 
61
- - [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, transitions, portal, text interpolation, events
62
- - [Components](./docs/runtime/component.md) — `defineComponent`, typed props/emits/refs, slots
63
- - [Lazy Loading](./docs/runtime/lazy.md) — `createLazyComponent`, `d-lazy`, `createSuspense` wrapper, code splitting
64
- - [Error Boundary](./docs/runtime/boundary.md) — `createErrorBoundary`, `createErrorBoundaryState`, `withErrorBoundary`, `d-boundary`
65
- - [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
66
-
67
- ### Routing
68
-
69
- - [Router](./docs/router.md) — Client-side routing with nested layouts, preloading, and file-based route generation
70
- - [Template Check CLI](./docs/cli/check.md) — `dalila check` static analysis for template/context consistency
71
-
72
- ### UI Components
73
-
74
- - [UI Components](./docs/ui.md) — Interactive components (Dialog, Drawer, Toast, Tabs, Calendar, etc.) with native HTML and full ARIA support
75
-
76
- ### Rendering
77
-
78
- - [when](./docs/core/when.md) — Conditional visibility
79
- - [match](./docs/core/match.md) — Switch-style rendering
80
- - [for](./docs/core/for.md) — List rendering with keyed diffing
81
- - [Virtual Lists](./docs/core/virtual.md) — Fixed and dynamic-height windowed rendering with infinite-scroll hooks
82
-
83
- ### Data
84
-
85
- - [Resources](./docs/core/resource.md) — Async data with loading/error states
86
- - [Query](./docs/core/query.md) — Cached queries
87
- - [Mutations](./docs/core/mutation.md) — Write operations
88
-
89
- ### HTTP
90
-
91
- - [HTTP Client](./docs/http.md) — Native fetch-based client with XSRF protection and interceptors
70
+ - [Template Binding](./docs/runtime/bind.md)
71
+ - [Components](./docs/runtime/component.md)
72
+ - [Lazy Loading](./docs/runtime/lazy.md)
73
+ - [Error Boundary](./docs/runtime/boundary.md)
74
+ - [FOUC Prevention](./docs/runtime/fouc-prevention.md)
92
75
 
93
- ### Forms
76
+ ### Rendering & Data
94
77
 
95
- - [Forms](./docs/forms.md) — DOM-first form management with validation, field arrays, and accessibility
78
+ - [when](./docs/core/when.md)
79
+ - [match](./docs/core/match.md)
80
+ - [for](./docs/core/for.md)
81
+ - [Virtual Lists](./docs/core/virtual.md)
82
+ - [Resources](./docs/core/resource.md)
83
+ - [Query](./docs/core/query.md)
84
+ - [Mutations](./docs/core/mutation.md)
96
85
 
97
- ### Utilities
86
+ ### Tooling
98
87
 
99
- - [Scheduler](./docs/core/scheduler.md) — Batching and coordination
100
- - [Keys](./docs/core/key.md) — Cache key encoding
101
- - [Dev Mode](./docs/core/dev.md) — Warnings and helpers
102
- - [Devtools Extension](./devtools-extension/README.md) — Browser panel for reactive graph and scopes
88
+ - [Template Check CLI](./docs/cli/check.md)
89
+ - [Devtools Extension](./devtools-extension/README.md)
103
90
 
104
91
  Firefox extension workflows:
105
92
 
106
93
  - `npm run devtools:firefox:run` — launch Firefox with extension loaded for dev
107
94
  - `npm run devtools:firefox:build` — package extension artifact for submission/signing
108
95
 
109
- ## Features
110
-
111
- ```
112
- dalila → signal, computed, effect, batch, ...
113
- dalila/runtime → bind(), mount(), configure(), createPortalTarget(), defineComponent()
114
- dalila/context → createContext, provide, inject
115
- dalila/http → createHttpClient with XSRF protection
116
- ```
117
-
118
- ### Signals
119
-
120
- ```ts
121
- import { signal, computed, effect, readonly, debounceSignal, throttleSignal } from 'dalila';
122
-
123
- const count = signal(0);
124
- const doubled = computed(() => count() * 2);
125
- const countRO = readonly(count);
126
- const search = signal('');
127
- const debouncedSearch = debounceSignal(search, 250);
128
- const scrollY = signal(0);
129
- const throttledScrollY = throttleSignal(scrollY, 16);
130
-
131
- effect(() => {
132
- console.log('Count is', countRO());
133
- console.log('Search after pause', debouncedSearch());
134
- console.log('Scroll sample', throttledScrollY());
135
- });
136
-
137
- count.set(5); // logs: Count is 5
138
- ```
139
-
140
- ### Template Binding
141
-
142
- ```ts
143
- import { configure, mount } from 'dalila/runtime';
144
-
145
- // Global component registry and options
146
- configure({ components: [MyComponent] });
147
-
148
- // Bind a selector to a reactive view-model
149
- const dispose = mount('.app', { count: signal(0) });
150
-
151
- // Cleanup when done
152
- dispose();
153
- ```
154
-
155
- ### Transitions and Portal
156
-
157
- ```html
158
- <div d-when="open" d-transition="fade">Panel</div>
159
- <div d-portal="showModal ? '#modal-root' : null">Modal content</div>
160
- ```
161
-
162
- ```ts
163
- import { configure, createPortalTarget } from 'dalila/runtime';
164
-
165
- const modalTarget = createPortalTarget('modal-root');
166
-
167
- configure({
168
- transitions: [{ name: 'fade', duration: 250 }],
169
- });
170
- ```
171
-
172
- ### Scopes
173
-
174
- ```ts
175
- import { createScope, withScope, effect } from 'dalila';
176
-
177
- const scope = createScope();
178
-
179
- withScope(scope, () => {
180
- effect(() => { /* auto-cleaned when scope disposes */ });
181
- });
182
-
183
- scope.dispose(); // stops all effects
184
- ```
185
-
186
- ### Context
187
-
188
- ```ts
189
- import { createContext, provide, inject } from 'dalila';
190
-
191
- const ThemeContext = createContext<'light' | 'dark'>('theme');
192
-
193
- // In parent scope
194
- provide(ThemeContext, 'dark');
195
-
196
- // In child scope
197
- const theme = inject(ThemeContext); // 'dark'
198
- ```
199
-
200
- ### Persist
201
-
202
- ```ts
203
- import { signal, persist } from 'dalila';
204
-
205
- // Auto-saves to localStorage
206
- const theme = persist(signal('dark'), { name: 'app-theme' });
207
-
208
- theme.set('light'); // Saved automatically
209
- // On reload: theme starts as 'light'
210
- ```
211
-
212
- ### HTTP Client
213
-
214
- ```ts
215
- import { createHttpClient } from 'dalila/http';
216
-
217
- const http = createHttpClient({
218
- baseURL: 'https://api.example.com',
219
- xsrf: true, // XSRF protection
220
- onError: (error) => {
221
- if (error.status === 401) window.location.href = '/login';
222
- throw error;
223
- }
224
- });
225
-
226
- // GET request
227
- const response = await http.get('/users');
228
- console.log(response.data);
229
-
230
- // POST with auto JSON serialization
231
- await http.post('/users', { name: 'John', email: 'john@example.com' });
232
- ```
233
-
234
- ### File-Based Routing
96
+ ## Packages
235
97
 
236
98
  ```txt
237
- src/app/
238
- ├── layout.html
239
- ├── page.html
240
- ├── about/
241
- │ └── page.html
242
- └── users/
243
- └── [id]/
244
- └── page.html
245
- ```
246
-
247
- ```bash
248
- npx dalila routes generate
249
- ```
250
-
251
- ```ts
252
- import { createRouter } from 'dalila/router';
253
- import { routes } from './routes.generated.js';
254
- import { routeManifest } from './routes.generated.manifest.js';
255
-
256
- const router = createRouter({
257
- outlet: document.getElementById('app')!,
258
- routes,
259
- routeManifest
260
- });
261
-
262
- router.start();
263
- ```
264
-
265
- ### Forms
266
-
267
- ```ts
268
- import { createForm } from 'dalila';
269
-
270
- const userForm = createForm({
271
- defaultValues: { name: '', email: '' },
272
- validate: (data) => {
273
- const errors: Record<string, string> = {};
274
- if (!data.name) errors.name = 'Name is required';
275
- if (!data.email?.includes('@')) errors.email = 'Invalid email';
276
- return errors;
277
- }
278
- });
279
-
280
- async function handleSubmit(data, { signal }) {
281
- await fetch('/api/users', {
282
- method: 'POST',
283
- body: JSON.stringify(data),
284
- signal
285
- });
286
- }
287
- ```
288
-
289
- ```html
290
- <form d-form="userForm" d-on-submit="handleSubmit">
291
- <label>
292
- Name
293
- <input d-field="name" />
294
- </label>
295
- <span d-error="name"></span>
296
-
297
- <label>
298
- Email
299
- <input d-field="email" type="email" />
300
- </label>
301
- <span d-error="email"></span>
302
-
303
- <button type="submit">Save</button>
304
- <span d-form-error="userForm"></span>
305
- </form>
306
- ```
307
-
308
- ### UI Components with Router
309
-
310
- ```ts
311
- // page.ts
312
- import { computed, signal } from 'dalila';
313
- import { createDialog, mountUI } from 'dalila/components/ui';
314
-
315
- class HomePageVM {
316
- count = signal(0);
317
- status = computed(() => `Count: ${this.count()}`);
318
- dialog = createDialog({ closeOnBackdrop: true, closeOnEscape: true });
319
- increment = () => this.count.update(n => n + 1);
320
- openModal = () => this.dialog.show();
321
- closeModal = () => this.dialog.close();
322
- }
323
-
324
- export function loader() {
325
- return new HomePageVM();
326
- }
327
-
328
- // Called after view is mounted
329
- export function onMount(root: HTMLElement, data: HomePageVM) {
330
- return mountUI(root, {
331
- dialogs: { dialog: data.dialog },
332
- events: []
333
- });
334
- }
335
-
336
- // Optional extra hook on route leave
337
- export function onUnmount(_root: HTMLElement) {}
338
- ```
339
-
340
- ```html
341
- <!-- page.html -->
342
- <d-button d-on-click="increment">Increment</d-button>
343
- <d-button d-on-click="openModal">Open Dialog</d-button>
344
-
345
- <d-dialog d-ui="dialog">
346
- <d-dialog-header>
347
- <d-dialog-title>{status}</d-dialog-title>
348
- <d-dialog-close d-on-click="closeModal">&times;</d-dialog-close>
349
- </d-dialog-header>
350
- <d-dialog-body>
351
- <p>Modal controlled by the official route + UI pattern.</p>
352
- </d-dialog-body>
353
- </d-dialog>
99
+ dalila → signals, scope, persist, forms, resources, query, mutations
100
+ dalila/runtime → bind(), mount(), configure(), components, lazy, transitions
101
+ dalila/context → createContext(), provide(), inject()
102
+ dalila/router → createRouter(), file-based routes, preloading
103
+ dalila/http → createHttpClient()
354
104
  ```
355
105
 
356
106
  ## Development
@@ -464,32 +464,32 @@ function pageProps(pageHtml, pageTs) {
464
464
  if (hasNamedExport(pageTs, 'validation')) {
465
465
  props.push(`validation: ${moduleExport(pageTs, 'validation', { allowValue: true })}`);
466
466
  }
467
- if (hasNamedExport(pageTs, 'onMount')) {
467
+ if (hasNamedExport(pageTs, 'onRouteMount')) {
468
468
  if (pageTs.lazy) {
469
469
  const lazyLoader = `${pageTs.importName}_lazy`;
470
- props.push(`onMount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
471
- if (typeof (mod as any).onMount === 'function') {
472
- return (mod as any).onMount(root, data, ctx);
470
+ props.push(`onRouteMount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
471
+ if (typeof (mod as any).onRouteMount === 'function') {
472
+ return (mod as any).onRouteMount(root, data, ctx);
473
473
  }
474
474
  return undefined;
475
475
  })`);
476
476
  }
477
477
  else {
478
- props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
478
+ props.push(`onRouteMount: ${moduleExport(pageTs, 'onRouteMount')}`);
479
479
  }
480
480
  }
481
- if (hasNamedExport(pageTs, 'onUnmount')) {
481
+ if (hasNamedExport(pageTs, 'onRouteUnmount')) {
482
482
  if (pageTs.lazy) {
483
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);
484
+ props.push(`onRouteUnmount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
485
+ if (typeof (mod as any).onRouteUnmount === 'function') {
486
+ return (mod as any).onRouteUnmount(root, data, ctx);
487
487
  }
488
488
  return undefined;
489
489
  })`);
490
490
  }
491
491
  else {
492
- props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
492
+ props.push(`onRouteUnmount: ${moduleExport(pageTs, 'onRouteUnmount')}`);
493
493
  }
494
494
  }
495
495
  }
@@ -516,11 +516,11 @@ function pageProps(pageHtml, pageTs) {
516
516
  if (hasNamedExport(pageTs, 'validation')) {
517
517
  props.push(`validation: ${moduleExport(pageTs, 'validation', { allowValue: true })}`);
518
518
  }
519
- if (hasNamedExport(pageTs, 'onMount')) {
520
- props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
519
+ if (hasNamedExport(pageTs, 'onRouteMount')) {
520
+ props.push(`onRouteMount: ${moduleExport(pageTs, 'onRouteMount')}`);
521
521
  }
522
- if (hasNamedExport(pageTs, 'onUnmount')) {
523
- props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
522
+ if (hasNamedExport(pageTs, 'onRouteUnmount')) {
523
+ props.push(`onRouteUnmount: ${moduleExport(pageTs, 'onRouteUnmount')}`);
524
524
  }
525
525
  return props;
526
526
  }
@@ -1,6 +1,6 @@
1
1
  export * from "./scope.js";
2
2
  export * from "./signal.js";
3
- export { watch, onMount, onCleanup, useEvent, useInterval, useTimeout, useFetch } from "./watch.js";
3
+ export { watch, onCleanup, useEvent, useInterval, useTimeout, useFetch } from "./watch.js";
4
4
  export * from "./when.js";
5
5
  export * from "./match.js";
6
6
  export * from "./for.js";
@@ -1,6 +1,6 @@
1
1
  export * from "./scope.js";
2
2
  export * from "./signal.js";
3
- export { watch, onMount, onCleanup, useEvent, useInterval, useTimeout, useFetch } from "./watch.js";
3
+ export { watch, onCleanup, useEvent, useInterval, useTimeout, useFetch } from "./watch.js";
4
4
  export * from "./when.js";
5
5
  export * from "./match.js";
6
6
  export * from "./for.js";
@@ -20,6 +20,15 @@
20
20
  * - Flushes always drain with `splice(0)` to release references eagerly.
21
21
  */
22
22
  type Task = () => void;
23
+ export type SchedulerPriority = 'high' | 'medium' | 'low';
24
+ export interface ScheduleOptions {
25
+ /** Priority used by RAF/microtask queues. Default: 'medium'. */
26
+ priority?: SchedulerPriority;
27
+ }
28
+ interface SchedulerPriorityContextOptions {
29
+ /** Warn when `fn` returns a promise. Default: true. */
30
+ warnOnAsync?: boolean;
31
+ }
23
32
  export interface TimeSliceOptions {
24
33
  /** Time budget per slice in milliseconds. Default: 8 */
25
34
  budgetMs?: number;
@@ -71,7 +80,7 @@ export declare function getSchedulerConfig(): Readonly<SchedulerConfig>;
71
80
  * Use this for DOM-affecting work you want grouped per frame.
72
81
  * (In Node tests, `requestAnimationFrame` is typically mocked.)
73
82
  */
74
- export declare function schedule(task: Task): void;
83
+ export declare function schedule(task: Task, options?: ScheduleOptions): void;
75
84
  /**
76
85
  * Schedule work in a microtask.
77
86
  *
@@ -79,7 +88,7 @@ export declare function schedule(task: Task): void;
79
88
  * - run after the current call stack,
80
89
  * - but before the next frame.
81
90
  */
82
- export declare function scheduleMicrotask(task: Task): void;
91
+ export declare function scheduleMicrotask(task: Task, options?: ScheduleOptions): void;
83
92
  /**
84
93
  * Returns true while inside a `batch()` call.
85
94
  *
@@ -108,6 +117,14 @@ export declare function queueInBatch(task: Task): void;
108
117
  * - We flush batched tasks in *one RAF* to group visible DOM work per frame.
109
118
  */
110
119
  export declare function batch(fn: () => void): void;
120
+ /**
121
+ * Run work under a temporary scheduler priority context.
122
+ *
123
+ * Sync-only helper: tasks scheduled without explicit priority inside the
124
+ * synchronous body of `fn` inherit this priority.
125
+ * Useful for framework internals (e.g. user input event handlers).
126
+ */
127
+ export declare function withSchedulerPriority<T>(priority: SchedulerPriority, fn: () => T, options?: SchedulerPriorityContextOptions): T;
111
128
  /**
112
129
  * DOM read discipline helper.
113
130
  *