@livenetworks/ashlar 2.1.12 → 2.2.0

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.
Files changed (108) hide show
  1. package/README.md +25 -2
  2. package/components/COMPONENTS.md +29 -25
  3. package/components/ln-api-connector/ln-api-connector.schema.json +105 -105
  4. package/components/ln-chart/ln-chart-dev.scss +0 -4
  5. package/components/ln-core/README.md +16 -60
  6. package/components/ln-core/index.js +1 -1
  7. package/components/ln-core/ln-core.schema.json +120 -120
  8. package/components/ln-core/query.js +113 -113
  9. package/components/ln-core/reactive.js +0 -59
  10. package/components/ln-data-coordinator/README.md +2 -0
  11. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  12. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +196 -196
  13. package/components/ln-data-coordinator/src/ln-data-coordinator.js +457 -432
  14. package/components/ln-data-store/ln-data-store.schema.json +92 -92
  15. package/components/ln-date/ln-date.js +1 -1
  16. package/components/ln-date/src/ln-date.js +1 -1
  17. package/components/ln-debug/ln-debug.js +1 -1
  18. package/components/ln-debug/ln-debug.schema.json +28 -28
  19. package/components/ln-debug/src/generated-attributes.js +304 -302
  20. package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
  21. package/components/ln-external-links/README.md +19 -19
  22. package/components/ln-external-links/ln-external-links.js +1 -1
  23. package/components/ln-external-links/ln-external-links.schema.json +9 -0
  24. package/components/ln-external-links/ln-external-links.scss +12 -0
  25. package/components/ln-external-links/src/ln-external-links.js +3 -2
  26. package/components/ln-filter/README.md +2 -1
  27. package/components/ln-filter/ln-filter.js +1 -1
  28. package/components/ln-filter/ln-filter.schema.json +157 -169
  29. package/components/ln-filter/src/ln-filter.js +4 -4
  30. package/components/ln-form/ln-form.schema.json +57 -57
  31. package/components/ln-key/ln-key.js +1 -1
  32. package/components/ln-key/ln-key.schema.json +8 -0
  33. package/components/ln-key/src/ln-key.js +8 -5
  34. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  35. package/components/ln-obfuscator/ln-obfuscator.schema.json +6 -0
  36. package/components/ln-obfuscator/src/ln-obfuscator.js +2 -2
  37. package/components/ln-options/ln-options.schema.json +47 -47
  38. package/components/ln-popover/ln-popover.schema.json +82 -82
  39. package/components/ln-progress/ln-progress.schema.json +0 -13
  40. package/components/ln-search/ln-search.schema.json +111 -111
  41. package/components/ln-sort/README.md +1 -0
  42. package/components/ln-sort/ln-sort.js +1 -1
  43. package/components/ln-sort/ln-sort.schema.json +100 -112
  44. package/components/ln-sort/src/sort-model.js +1 -6
  45. package/components/ln-sortable/ln-sortable.schema.json +38 -38
  46. package/components/ln-sortable/ln-sortable.scss +25 -25
  47. package/components/ln-table/README.md +8 -1
  48. package/components/ln-table/ln-table.js +1 -1
  49. package/components/ln-table/ln-table.schema.json +195 -195
  50. package/components/ln-table/src/ln-table.js +43 -7
  51. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  52. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +18 -0
  53. package/components/ln-table-coordinator/src/ln-table-coordinator.js +104 -2
  54. package/components/ln-toast/README.md +1 -1
  55. package/components/ln-toast/ln-toast.js +1 -1
  56. package/components/ln-toast/ln-toast.schema.json +6 -0
  57. package/components/ln-toast/src/ln-toast.js +1 -1
  58. package/components/ln-validate/README.md +6 -6
  59. package/components/ln-validate/ln-validate-dev.scss +0 -5
  60. package/components/ln-validate/ln-validate.js +1 -1
  61. package/components/ln-validate/ln-validate.schema.json +0 -6
  62. package/components/ln-validate/src/ln-validate.js +48 -29
  63. package/ln-ashlar.html-data.json +1824 -1814
  64. package/package.json +15 -5
  65. package/theme/README.md +74 -0
  66. package/theme/base/_global.scss +2 -2
  67. package/theme/base/_reset.scss +11 -0
  68. package/theme/components/_app-shell.scss +0 -8
  69. package/theme/components/_form.scss +6 -6
  70. package/theme/components/_ln-table.scss +6 -3
  71. package/theme/components/_scrollbar.scss +5 -4
  72. package/theme/components/_sortable.scss +19 -19
  73. package/theme/components/_stat-card.scss +1 -0
  74. package/theme/components/_table.scss +11 -35
  75. package/theme/config/_density.scss +110 -65
  76. package/theme/config/_palette.scss +71 -49
  77. package/theme/config/_theme.scss +27 -31
  78. package/theme/config/_tokens.scss +28 -26
  79. package/theme/config/mixins/_app-shell.scss +17 -21
  80. package/theme/config/mixins/_avatar.scss +2 -2
  81. package/theme/config/mixins/_btn.scss +4 -2
  82. package/theme/config/mixins/_card.scss +88 -71
  83. package/theme/config/mixins/_colors.scss +2 -1
  84. package/theme/config/mixins/_display.scss +12 -0
  85. package/theme/config/mixins/_footer.scss +3 -1
  86. package/theme/config/mixins/_form.scss +74 -55
  87. package/theme/config/mixins/_layout.scss +9 -0
  88. package/theme/config/mixins/_ln-table.scss +22 -15
  89. package/theme/config/mixins/_modal.scss +3 -0
  90. package/theme/config/mixins/_nav.scss +31 -8
  91. package/theme/config/mixins/_sidebar.scss +2 -1
  92. package/theme/config/mixins/_spacing.scss +4 -4
  93. package/theme/config/mixins/_stat-card.scss +33 -2
  94. package/theme/config/mixins/_status-badge.scss +1 -0
  95. package/theme/config/mixins/_table.scss +87 -69
  96. package/theme/config/mixins/_tabs.scss +31 -0
  97. package/theme/config/mixins/_toast.scss +0 -1
  98. package/theme/designs/relaxed/_card.scss +12 -0
  99. package/theme/designs/relaxed/_layout.scss +24 -0
  100. package/theme/ln-ashlar-core.scss +1 -0
  101. package/theme/ln-ashlar-dev.scss +0 -1
  102. package/theme/ln-ashlar-relaxed.scss +67 -0
  103. package/theme/tailwind/components.css +2198 -0
  104. package/theme/tailwind/entry.css +3 -0
  105. package/theme/tailwind/theme.css +23 -0
  106. package/theme/utilities/_utilities.scss +1 -11
  107. package/web-types.json +2351 -2337
  108. package/components/ln-progress/ln-progress-dev.scss +0 -8
package/README.md CHANGED
@@ -107,7 +107,7 @@ Combine autonomous components directly in semantic HTML — **zero initializatio
107
107
 
108
108
  ### 1. 🏛️ Dual-Core Architecture (SSR + Client-Side SPA)
109
109
  - **Backend-First Progressive HTML Enhancement (SSR):** Your backend (Laravel, Go, Django, Rails) delivers indexable, semantic HTML. Elements auto-activate via attributes (`data-ln-modal`, `data-ln-filter`, `data-ln-table`).
110
- - **Client-Side Single-Page Applications (SPA / JSON Mode):** Built-in client router (`ln-router`), compound hash-state codec (`hash.js`), 3-tier local storage (`ln-data-store`), and microtask-batched Proxy reactivity (`deepReactive`, `renderList`) when client JSON APIs are required.
110
+ - **Client-Side Single-Page Applications (SPA / JSON Mode):** Built-in client router (`ln-router`), compound hash-state codec (`hash.js`), 3-tier local storage (`ln-data-store`), and microtask-batched state rendering (`createBatcher`, `renderList`) when client JSON APIs are required.
111
111
 
112
112
  ### 2. 🤖 AI-Native & MCP Surface
113
113
  - **Machine-Readable Component Corpus (`docs-mcp/`):** Schema-validated documentation served directly to AI agent workflows via Model Context Protocol (MCP).
@@ -184,7 +184,7 @@ Deep architectural blueprints detailing the engine driving `ln-ashlar`.
184
184
  | 📖 **[Architecture Philosophy](docs/architecture/philosophy.md)** | The DOM-First engineering manifesto detailing computing cycles, framework EOL risks, performance, security, **and full 17-category comparison tables**. |
185
185
  | 🔄 **[Data Flow Architecture](docs/architecture/data-flow.md)** | Rules governing how data moves. Splits responsibilities into **four isolated concerns**: Data (`ln-data-store` + `ln-data-coordinator`), Render (`ln-table`), Submit (`ln-form`), and Validate (`ln-validate`). Details the **parallel fan-out write pipeline** with Mermaid sequence diagrams. |
186
186
  | 🧭 **[3-Tier Local-First Storage Specs](docs/architecture/data-store-architecture.md)** | Technical reference for decoupling storage caches (`ln-data-store`), network gateways (`ln-*-connector`), and data transformations (`ln-mapper`). |
187
- | ⚡ **[Reactive Architecture Reference](docs/reactive.md)** | Blueprint on how components manage internal state using Proxy traps (`reactiveState` and `deepReactive`), batched microtask rendering, DOM bindings (`fill`, `renderList`), and attribute-to-state bridges. |
187
+ | ⚡ **[Reactive Architecture Reference](docs/reactive.md)** | Blueprint on how components manage state using microtask batching (`createBatcher`), DOM bindings (`fill`, `renderList`), and attribute-to-state bridges. |
188
188
  | 📐 **[Design System Specifications](docs/architecture/reference.md)** | Complete styling specs covering CSS Custom Properties, layout grids, buttons, responsive breakpoints, typography hierarchies, dark mode theming, and icons. |
189
189
  | 🛡️ **[Security & Threat Mitigation](docs/architecture/security.md)** | Deep architectural security analysis covering Web Crypto API encryption-at-rest, strict CSP compliance (no dynamic eval), sensitive DOM attribute protections, and the same-origin AJAX fragment trust boundary. |
190
190
  | 🧠 **[Architect Overview Guide](docs/architecture/overview.md)** | Master developer guide introducing the DOM-First doctrine, component inventories, override architectures, and new project integration pipelines. |
@@ -270,6 +270,29 @@ For detailed manual instructions, properties, attributes, and events of individu
270
270
 
271
271
  ---
272
272
 
273
+ ---
274
+
275
+ ## 🎨 Dual CSS Engine Architecture
276
+
277
+ `ln-ashlar` offers dual-engine visual styling with 100% design parity across all components and layouts:
278
+
279
+ 1. **Native Ashlar SCSS:** Class-free semantic HTML styling and design tokens. Optimized for minimal CSSOM invalidation by eliminating mixin selector explosions and `:not(:has())` bottlenecks.
280
+ 2. **Tailwind CSS v4 Engine:** Utility-first stylesheet compiled directly with Tailwind v4 `@theme` tokens (`npm run build:tailwind`).
281
+
282
+ See [Theme Architecture & SCSS Guidelines](theme/README.md) for details.
283
+
284
+ ---
285
+
286
+ ## 🧪 Automated Testing & Performance Suite
287
+
288
+ `ln-ashlar` includes an automated headless verification and benchmarking harness comparing both CSS engines:
289
+
290
+ - **Visual & Design Suite (`npm run test:visual`):** Validates responsive layout, horizontal overflow, and reactive UI across all 67 demo admin pages.
291
+ - **JS Performance Suite (`npm run test:perf`):** Benchmarks execution latency, DOM node churn, and JS heap memory across 46 interactive JS components.
292
+ - **Historical Comparison (`npm run test:perf:compare`):** Tracks performance trends across git commits and alerts on performance regressions (`>20%`).
293
+
294
+ See [Headless Testing Documentation](tests/headless/README.md) for full usage and options.
295
+
273
296
  ## 🖥️ Interactive Demo Site
274
297
 
275
298
  `ln-ashlar` ships with a complete local dashboard environment inside `demo/admin/`. The demo is itself a project consuming `ln-ashlar`, showing real-world layout structures, component setups, and customized semantic SCSS:
@@ -69,7 +69,7 @@ Every component is an IIFE (Immediately Invoked Function Expression). Shared hel
69
69
 
70
70
  ```javascript
71
71
  import { dispatch } from '../ln-core';
72
- import { deepReactive, createBatcher } from '../ln-core';
72
+ import { createBatcher } from '../ln-core';
73
73
 
74
74
  (function () {
75
75
  const DOM_SELECTOR = 'data-ln-{name}';
@@ -96,8 +96,6 @@ import { deepReactive, createBatcher } from '../ln-core';
96
96
  | `fillTemplate(clone, data)` | Replace `{{ key }}` text-node placeholders in cloned template with `data[key]` values |
97
97
  | `buildDict(root, selector)` | Read hidden i18n elements once at init, return plain object, remove from DOM |
98
98
  | `renderList(container, items, tpl, keyFn, fillFn, tag)` | Keyed list rendering with DOM reuse |
99
- | `reactiveState(initial, onChange)` | Shallow Proxy — onChange(prop, value, old) per set |
100
- | `deepReactive(obj, onChange)` | Deep Proxy — onChange() on any nested change |
101
99
  | `createBatcher(renderFn, afterRender)` | Coalesce multiple sync state changes into one render |
102
100
 
103
101
  Import only what the component needs. Vite tree-shakes unused exports.
@@ -215,31 +213,38 @@ function _domObserver() {
215
213
 
216
214
  ## Reactive Rendering Pattern
217
215
 
218
- When a component has **multiple internal state properties** that together drive DOM updates, use `reactiveState` + `createBatcher` + `fill()` from ln-core. This eliminates manual querySelector chains and coalesces multiple synchronous state changes into a single DOM update.
216
+ When a component has **multiple internal state properties** or frequent updates that drive DOM updates, use `createBatcher` + `fill()` from ln-core. This eliminates manual querySelector chains and coalesces multiple synchronous state changes into a single DOM update.
219
217
 
220
- ### The three primitives
218
+ ### The core primitives
221
219
 
222
220
  | Helper | Role |
223
221
  |--------|------|
224
- | `reactiveState(initial, onChange)` | Proxy — calls `onChange(prop, value, old)` on every top-level property set |
225
222
  | `createBatcher(renderFn, afterRender)` | Coalesces sync state changes into one `renderFn` call via `queueMicrotask` |
226
223
  | `fill(root, data)` | Writes state to DOM via `data-ln-field`, `data-ln-attr`, `data-ln-show`, `data-ln-class` |
227
224
 
228
225
  ### Pattern
229
226
 
230
227
  ```javascript
231
- import { reactiveState, createBatcher, fill, dispatch } from '../ln-core';
228
+ import { createBatcher, fill, dispatch } from '../ln-core';
232
229
 
233
230
  function _component(dom) {
234
231
  this.dom = dom;
235
232
  const self = this;
236
233
 
237
- const queueRender = createBatcher(
238
- function () { self._render(); }, // runs after current sync block
239
- function () { self._afterRender(); } // runs after render — dispatch events here
240
- );
234
+ this.state = { key: null, value: null };
241
235
 
242
- this.state = reactiveState({ key: null, value: null }, queueRender);
236
+ this.queueRender = createBatcher(
237
+ function () {
238
+ if (!self._destroyed) {
239
+ self._render();
240
+ }
241
+ }, // runs after current sync block
242
+ function () {
243
+ if (!self._destroyed) {
244
+ self._afterRender();
245
+ }
246
+ } // runs after render — dispatch events here
247
+ );
243
248
  }
244
249
 
245
250
  _component.prototype._render = function () {
@@ -250,31 +255,30 @@ _component.prototype._render = function () {
250
255
  _component.prototype._afterRender = function () {
251
256
  dispatch(this.dom, 'ln-component:changed', { key: this.state.key });
252
257
  };
258
+
259
+ _component.prototype.update = function (key, value) {
260
+ this.state.key = key;
261
+ this.state.value = value;
262
+ this.queueRender(); // Multiple sync updates trigger exactly ONE render pass
263
+ };
253
264
  ```
254
265
 
255
266
  When multiple properties change in the same sync block:
256
267
 
257
268
  ```javascript
258
- this.state.key = 'status'; // queues render
259
- this.state.value = 'active'; // queues again — same microtask, no duplicate
269
+ this.state.key = 'status';
270
+ this.state.value = 'active';
271
+ this.queueRender();
260
272
  // → microtask fires → _render() once → _afterRender() once
261
273
  ```
262
274
 
263
- ### `reactiveState` vs `deepReactive`
264
-
265
- | | `reactiveState` | `deepReactive` |
266
- |---|---|---|
267
- | **Depth** | Shallow — top-level sets only | Deep — nested object/array mutations |
268
- | **onChange args** | `(prop, value, old)` | `()` — no args |
269
- | **Use when** | Flat state with primitive values | State contains nested objects or arrays |
270
-
271
275
  ### `fill()` — limitations
272
276
 
273
277
  `fill()` sets `textContent` for `data-ln-field` — it does **not** set `.value` on `<input>`, `<textarea>`, or `<select>`. For form elements, set `.value` manually after `fill()`.
274
278
 
275
279
  ### When to use
276
280
 
277
- Use reactive rendering when a component has **2+ state properties** that together drive DOM, and multiple changes happen in the same sync block.
281
+ Use `createBatcher` when a component has **frequent or multiple state updates** (e.g. search input, filter checkboxes, sorting), and multiple updates can occur in the same synchronous cycle.
278
282
 
279
283
  **Don't use it for:**
280
284
  - Single boolean state — `this.isOpen = true; dom.classList.toggle(...)` is simpler
@@ -283,7 +287,7 @@ Use reactive rendering when a component has **2+ state properties** that togethe
283
287
 
284
288
  ### Used in
285
289
 
286
- *No library component currently uses this pattern. It remains documented for project-level components that need multi-property reactive state.*
290
+ Used in `ln-search`, `ln-filter`, `ln-list`, and `ln-data-coordinator` for microtask render coalescing.
287
291
 
288
292
  ---
289
293
 
@@ -985,7 +989,7 @@ The component calls `_ensureDefaultItemTemplate()` at the top of `_initUpload()`
985
989
 
986
990
  | Component | Pattern | Data Attr | Description |
987
991
  |-----------|---------|-----------|------|
988
- | ln-core | Shared module | — | cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, fill, fillTemplate, renderList, buildDict, guardBody, findElements, reactiveState, deepReactive, createBatcher, createWindowCache |
992
+ | ln-core | Shared module | — | cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, fill, fillTemplate, renderList, buildDict, guardBody, findElements, createBatcher, createWindowCache |
989
993
  | ln-toggle | Instance | `data-ln-toggle` | Generic toggle (sidebar, collapse) |
990
994
  | ln-accordion | Instance | `data-ln-accordion` | Wrapper — only one toggle open at a time |
991
995
  | ln-tabs | Instance | `data-ln-tabs` | Tab navigation — URL hash (anchor triggers) or localStorage persist (button triggers) |
@@ -1,105 +1,105 @@
1
- {
2
- "$comment": "This file is partly generated. The $comment, component, generator, and each attribute's direction, sources, type, values, fallback, and description keys are owned by the generator and synchronized from JS source code. All other keys are preserved and safe to hand-author. Regenerate with `npm run sync:ln-schemas`.",
3
- "component": "ln-api-connector",
4
- "generator": "scripts/sync-ln-schemas.mjs",
5
- "attributes": {
6
- "data-ln-api-base-url": {
7
- "direction": null,
8
- "sources": [
9
- "src/ln-api-connector.js"
10
- ],
11
- "type": "string",
12
- "fallback": "",
13
- "description": "Base URL endpoint for API requests"
14
- },
15
- "data-ln-api-connector": {
16
- "direction": null,
17
- "sources": [
18
- "src/ln-api-connector.js"
19
- ],
20
- "type": "marker",
21
- "description": "Mounts API connector bridging REST backend and ln-ashlar data coordinators"
22
- },
23
- "data-ln-api-connector-query-debounce": {
24
- "direction": "author",
25
- "sources": [
26
- "src/ln-api-connector.js"
27
- ],
28
- "type": "integer",
29
- "fallback": 200,
30
- "min": 0,
31
- "description": "Debounce delay in milliseconds before dispatching query requests"
32
- },
33
- "data-ln-api-endpoint": {
34
- "direction": "author",
35
- "sources": [
36
- "src/ln-api-connector.js"
37
- ],
38
- "type": "string",
39
- "fallback": "",
40
- "description": "Alias for data-ln-api-path"
41
- },
42
- "data-ln-api-headers": {
43
- "direction": null,
44
- "sources": [
45
- "src/ln-api-connector.js"
46
- ],
47
- "type": "json",
48
- "description": "Custom HTTP headers in JSON format or semicolon-separated pairs"
49
- },
50
- "data-ln-api-param-limit": {
51
- "direction": "author",
52
- "sources": [
53
- "src/ln-api-connector.js"
54
- ],
55
- "type": "string",
56
- "fallback": "limit",
57
- "description": "Query parameter name for pagination page size"
58
- },
59
- "data-ln-api-param-offset": {
60
- "direction": "author",
61
- "sources": [
62
- "src/ln-api-connector.js"
63
- ],
64
- "type": "string",
65
- "fallback": "offset",
66
- "description": "Query parameter name for pagination offset"
67
- },
68
- "data-ln-api-param-search": {
69
- "direction": "author",
70
- "sources": [
71
- "src/ln-api-connector.js"
72
- ],
73
- "type": "string",
74
- "fallback": "search",
75
- "description": "Query parameter name for text search filter"
76
- },
77
- "data-ln-api-param-sort-dir": {
78
- "direction": "author",
79
- "sources": [
80
- "src/ln-api-connector.js"
81
- ],
82
- "type": "string",
83
- "fallback": "sort_dir",
84
- "description": "Query parameter name for sort direction"
85
- },
86
- "data-ln-api-param-sort-field": {
87
- "direction": "author",
88
- "sources": [
89
- "src/ln-api-connector.js"
90
- ],
91
- "type": "string",
92
- "fallback": "sort_by",
93
- "description": "Query parameter name for sort field"
94
- },
95
- "data-ln-api-path": {
96
- "direction": "author",
97
- "sources": [
98
- "src/ln-api-connector.js"
99
- ],
100
- "type": "string",
101
- "fallback": "",
102
- "description": "Resource path appended to base URL"
103
- }
104
- }
105
- }
1
+ {
2
+ "$comment": "This file is partly generated. The $comment, component, generator, and each attribute's direction, sources, type, values, fallback, and description keys are owned by the generator and synchronized from JS source code. All other keys are preserved and safe to hand-author. Regenerate with `npm run sync:ln-schemas`.",
3
+ "component": "ln-api-connector",
4
+ "generator": "scripts/sync-ln-schemas.mjs",
5
+ "attributes": {
6
+ "data-ln-api-base-url": {
7
+ "direction": null,
8
+ "sources": [
9
+ "src/ln-api-connector.js"
10
+ ],
11
+ "type": "string",
12
+ "fallback": "",
13
+ "description": "Base URL endpoint for API requests"
14
+ },
15
+ "data-ln-api-connector": {
16
+ "direction": null,
17
+ "sources": [
18
+ "src/ln-api-connector.js"
19
+ ],
20
+ "type": "marker",
21
+ "description": "Mounts API connector bridging REST backend and ln-ashlar data coordinators"
22
+ },
23
+ "data-ln-api-connector-query-debounce": {
24
+ "direction": "author",
25
+ "sources": [
26
+ "src/ln-api-connector.js"
27
+ ],
28
+ "type": "integer",
29
+ "fallback": 200,
30
+ "min": 0,
31
+ "description": "Debounce delay in milliseconds before dispatching query requests"
32
+ },
33
+ "data-ln-api-endpoint": {
34
+ "direction": "author",
35
+ "sources": [
36
+ "src/ln-api-connector.js"
37
+ ],
38
+ "type": "string",
39
+ "fallback": "",
40
+ "description": "Alias for data-ln-api-path"
41
+ },
42
+ "data-ln-api-headers": {
43
+ "direction": null,
44
+ "sources": [
45
+ "src/ln-api-connector.js"
46
+ ],
47
+ "type": "json",
48
+ "description": "Custom HTTP headers in JSON format or semicolon-separated pairs"
49
+ },
50
+ "data-ln-api-param-limit": {
51
+ "direction": "author",
52
+ "sources": [
53
+ "src/ln-api-connector.js"
54
+ ],
55
+ "type": "string",
56
+ "fallback": "limit",
57
+ "description": "Query parameter name for pagination page size"
58
+ },
59
+ "data-ln-api-param-offset": {
60
+ "direction": "author",
61
+ "sources": [
62
+ "src/ln-api-connector.js"
63
+ ],
64
+ "type": "string",
65
+ "fallback": "offset",
66
+ "description": "Query parameter name for pagination offset"
67
+ },
68
+ "data-ln-api-param-search": {
69
+ "direction": "author",
70
+ "sources": [
71
+ "src/ln-api-connector.js"
72
+ ],
73
+ "type": "string",
74
+ "fallback": "search",
75
+ "description": "Query parameter name for text search filter"
76
+ },
77
+ "data-ln-api-param-sort-dir": {
78
+ "direction": "author",
79
+ "sources": [
80
+ "src/ln-api-connector.js"
81
+ ],
82
+ "type": "string",
83
+ "fallback": "sort_dir",
84
+ "description": "Query parameter name for sort direction"
85
+ },
86
+ "data-ln-api-param-sort-field": {
87
+ "direction": "author",
88
+ "sources": [
89
+ "src/ln-api-connector.js"
90
+ ],
91
+ "type": "string",
92
+ "fallback": "sort_by",
93
+ "description": "Query parameter name for sort field"
94
+ },
95
+ "data-ln-api-path": {
96
+ "direction": "author",
97
+ "sources": [
98
+ "src/ln-api-connector.js"
99
+ ],
100
+ "type": "string",
101
+ "fallback": "",
102
+ "description": "Resource path appended to base URL"
103
+ }
104
+ }
105
+ }
@@ -1,10 +1,6 @@
1
1
  @use '../../theme/config/mixins' as *;
2
2
 
3
3
  [data-ln-debug] {
4
- [data-ln-chart]:not(.chart) {
5
- @include dev-dom-error("[data-ln-chart] is missing the required .chart visual class.");
6
- }
7
-
8
4
  [data-ln-chart]:not([data-ln-chart-x]),
9
5
  [data-ln-chart]:not([data-ln-chart-y]) {
10
6
  @include dev-dom-error("[data-ln-chart] requires data-ln-chart-x and data-ln-chart-y.");
@@ -13,7 +13,7 @@ ln-core exposes helpers in these categories:
13
13
  - **DOM binding** — declarative data-attribute-driven rendering (`fill`, `renderList`, `fillTemplate`)
14
14
  - **Forms & Inputs** — serialize/populate by field `name`, intercept input value getters/setters (`interceptValueProperty`)
15
15
  - **Target & Event Predicates** — `shouldIgnoreClick`, `isTargetDisabled`, `isUsableTarget`, `isEditableTarget`
16
- - **Reactivity** — `reactiveState`, `deepReactive`, `createBatcher`
16
+ - **Reactivity & Batching** — microtask-coalesced render batcher (`createBatcher`)
17
17
  - **Windowing** — sliding-window sparse cache for server-side virtualization (`createWindowCache`)
18
18
  - **Layout & Positioning** — viewport-aware positioning (`computePlacement`), measurement (`measureHidden`)
19
19
  - **Hash codec** — namespaced URL fragment state (`hashParse`, `hashGet`, `hashSet`, `hashSortEncode`, `hashSortDecode`, `hashFilterEncode`, `hashFilterDecode`, `resolveHashNamespace`)
@@ -542,37 +542,6 @@ fetch('/tpl.html').then(html => {
542
542
 
543
543
  ## reactive.js
544
544
 
545
- ### reactiveState(initial, onChange)
546
-
547
- Shallow `Proxy` — fires `onChange(prop, value, old)` on every property set. No-op if value hasn't changed.
548
-
549
- ```js
550
- const state = reactiveState({ searchTerm: '', sortCol: -1 }, function (prop, value, old) {
551
- batchRender();
552
- });
553
- ```
554
-
555
- - Returns a Proxy wrapping a shallow copy of `initial`
556
- - Only tracks direct property assignments (not nested objects)
557
- - Used by `ln-filter` for flat state
558
-
559
- ### deepReactive(obj, onChange)
560
-
561
- Deep `Proxy` — recursively wraps nested objects and arrays. Fires `onChange()` (no args) on any nested mutation.
562
-
563
- ```js
564
- const state = deepReactive({ toasts: [], files: [] }, function () {
565
- batchRender();
566
- });
567
- state.toasts.push({ title: 'Hello' }); // triggers onChange
568
- ```
569
-
570
- - Wraps existing nested objects on creation
571
- - Auto-wraps newly assigned objects/arrays
572
- - `deleteProperty` also triggers `onChange`
573
- - Internal `Symbol('deepReactive')` flag prevents double-wrapping
574
- - Used by `ln-toast`, `ln-upload` for complex state
575
-
576
545
  ### createBatcher(renderFn, afterRender)
577
546
 
578
547
  Coalesce multiple synchronous state changes into a single `renderFn` call via `queueMicrotask`.
@@ -586,39 +555,24 @@ const batchRender = createBatcher(
586
555
 
587
556
  - Multiple calls to `schedule()` within the same microtask result in one `renderFn` call
588
557
  - Optional `afterRender` callback fires after each render
589
- - Pattern: `deepReactive(state, batchRender)` — every state mutation schedules a batched render
558
+ - Pattern: used in `ln-search`, `ln-filter`, `ln-list`, `ln-data-coordinator` to debounce/coalesce state and render operations
590
559
 
591
560
  ---
592
561
 
593
562
  ## Reactive Rendering Pattern
594
563
 
595
- The three reactive primitives (`reactiveState` / `deepReactive` +
596
- `createBatcher` + `fill`) are designed to be used together. They are
597
- the canonical ln-ashlar render loop:
598
-
599
- 1. **State mutation.** Component code assigns to a property on a
600
- reactive proxy (`this.state.value = 'x'`). The proxy fires its
601
- onChange callback.
602
- 2. **Microtask queue.** The onChange is wired to a `createBatcher`
603
- schedule function. Any number of synchronous assignments in the
604
- same sync block schedule a single microtask.
605
- 3. **One render.** When the microtask fires, the batcher calls
606
- `renderFn` exactly once. `renderFn` typically calls `fill(this.dom,
607
- this.state)` (or `renderList` for keyed collections) to project
608
- state into the DOM, then the optional `afterRender` callback fires
609
- (commonly used to dispatch a `ln-{component}:rendered` event).
610
-
611
- Two assignments on the same tick produce one DOM update. External
612
- callers that need to set several properties at once do not need a
613
- `batch()` helper — the batcher handles it.
564
+ The `createBatcher` primitive and DOM rendering helpers (`fill`, `renderList`) are designed to be used together as the canonical ln-ashlar microtask render loop:
565
+
566
+ 1. **State mutation.** Component updates internal data properties (e.g. `this.state.key = 'val'`).
567
+ 2. **Microtask queue.** The component triggers the `schedule()` function returned by `createBatcher`. Any number of synchronous assignments in the same sync block schedule a single microtask.
568
+ 3. **One render.** When the microtask fires, the batcher calls `renderFn` exactly once. `renderFn` typically calls `fill(this.dom, this.state)` (or `renderList` for keyed collections) to project state into the DOM, then the optional `afterRender` callback fires (commonly used to dispatch a `ln-{component}:rendered` or changed event).
569
+
570
+ Two state assignments on the same tick produce one DOM update:
614
571
 
615
572
  The pattern is: **state mutation → microtask queue → one render**.
616
- Never wire a reactive onChange directly to `_render()` without a
617
- batcher between them — you will get one render per assignment, and
618
- the DOM will thrash.
573
+ Never wire sync state updates directly to immediate DOM writes when multiple attributes or filter parameters can change simultaneously — batching prevents layout thrashing.
619
574
 
620
- For the full decision matrix (`reactiveState` vs `deepReactive`, when
621
- to use this pattern at all, anti-patterns), see
575
+ For further details, see
622
576
  [Reactive Rendering Pattern in components/COMPONENTS.md](../../components/COMPONENTS.md#reactive-rendering-pattern).
623
577
 
624
578
  ---
@@ -981,7 +935,7 @@ Returns `true` if the element is an `<input>`, `<textarea>`, `<select>`, or `[co
981
935
  ## Typical Usage Pattern
982
936
 
983
937
  ```js
984
- import { dispatch, dispatchCancelable, guardBody, findElements, deepReactive, createBatcher, fill, renderList } from '../ln-core';
938
+ import { dispatch, dispatchCancelable, guardBody, findElements, createBatcher, fill, renderList } from '../ln-core';
985
939
 
986
940
  (function () {
987
941
  const DOM_SELECTOR = 'data-ln-example';
@@ -997,8 +951,10 @@ import { dispatch, dispatchCancelable, guardBody, findElements, deepReactive, cr
997
951
  const self = this;
998
952
 
999
953
  // Reactive state + batched rendering
1000
- const batchRender = createBatcher(function () { self._render(); });
1001
- this.state = deepReactive({ items: [] }, batchRender);
954
+ this.state = { items: [] };
955
+ this.batchRender = createBatcher(function () {
956
+ if (!self._destroyed) self._render();
957
+ });
1002
958
 
1003
959
  return this;
1004
960
  }
@@ -1,5 +1,5 @@
1
1
  export { cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, requestData, fill, lnFill, fillTemplate, renderList, buildDict, guardBody, findElements, isVisible, shouldIgnoreClick, isTargetDisabled, isUsableTarget, isEditableTarget, serializeForm, populateForm, resolveFormMethod, getLocale, readValue, detectValueType, compareValues, registerComponent, shouldInterceptLink, buildUrl, getHeaders, parseHeaders, registerDataMapper, getDataMapper, interceptValueProperty, registerLocaleFallback, getLocaleFallback, holdInit, releaseInit, pendingCount, queueBoot, ensureLocaleObserver, observeAttributes, observeLifecycle, withoutLifecycle, setDebugSink, setPersistSink, isDevMode, hasActiveDebug } from './helpers.js';
2
- export { reactiveState, deepReactive, createBatcher } from './reactive.js';
2
+ export { createBatcher } from './reactive.js';
3
3
  export { createWindowCache } from './window-cache.js';
4
4
  export { hashParse, hashGet, hashSet, hashLinkClick, resolveHashNamespace, hashSortEncode, hashSortDecode, hashFilterEncode, hashFilterDecode } from './hash.js';
5
5
  export { queryParse, queryGet, querySet, resolveQueryParam, querySortEncode, querySortDecode, queryFilterEncode, queryFilterDecode } from './query.js';