@livenetworks/ashlar 2.1.12 โ†’ 2.1.13

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
@@ -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.
@@ -213,80 +211,86 @@ function _domObserver() {
213
211
 
214
212
  ---
215
213
 
216
- ## Reactive Rendering Pattern
217
-
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.
219
-
220
- ### The three primitives
221
-
222
- | Helper | Role |
223
- |--------|------|
224
- | `reactiveState(initial, onChange)` | Proxy โ€” calls `onChange(prop, value, old)` on every top-level property set |
225
- | `createBatcher(renderFn, afterRender)` | Coalesces sync state changes into one `renderFn` call via `queueMicrotask` |
226
- | `fill(root, data)` | Writes state to DOM via `data-ln-field`, `data-ln-attr`, `data-ln-show`, `data-ln-class` |
227
-
228
- ### Pattern
229
-
230
- ```javascript
231
- import { reactiveState, createBatcher, fill, dispatch } from '../ln-core';
232
-
233
- function _component(dom) {
234
- this.dom = dom;
235
- const self = this;
236
-
237
- const queueRender = createBatcher(
238
- function () { self._render(); }, // runs after current sync block
239
- function () { self._afterRender(); } // runs after render โ€” dispatch events here
240
- );
241
-
242
- this.state = reactiveState({ key: null, value: null }, queueRender);
243
- }
244
-
245
- _component.prototype._render = function () {
246
- fill(this.dom, this.state);
247
- // additional DOM logic derived from state
248
- };
249
-
250
- _component.prototype._afterRender = function () {
251
- dispatch(this.dom, 'ln-component:changed', { key: this.state.key });
252
- };
253
- ```
254
-
255
- When multiple properties change in the same sync block:
256
-
257
- ```javascript
258
- this.state.key = 'status'; // queues render
259
- this.state.value = 'active'; // queues again โ€” same microtask, no duplicate
260
- // โ†’ microtask fires โ†’ _render() once โ†’ _afterRender() once
261
- ```
262
-
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
- ### `fill()` โ€” limitations
272
-
273
- `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
-
275
- ### When to use
276
-
277
- Use reactive rendering when a component has **2+ state properties** that together drive DOM, and multiple changes happen in the same sync block.
278
-
279
- **Don't use it for:**
280
- - Single boolean state โ€” `this.isOpen = true; dom.classList.toggle(...)` is simpler
281
- - Event-driven rendering โ€” components that receive data via `set-data` events (ln-table)
282
- - One-shot renders โ€” clone template + fill once, no ongoing state
283
-
284
- ### Used in
285
-
286
- *No library component currently uses this pattern. It remains documented for project-level components that need multi-property reactive state.*
287
-
288
- ---
289
-
214
+ ## Reactive Rendering Pattern
215
+
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.
217
+
218
+ ### The core primitives
219
+
220
+ | Helper | Role |
221
+ |--------|------|
222
+ | `createBatcher(renderFn, afterRender)` | Coalesces sync state changes into one `renderFn` call via `queueMicrotask` |
223
+ | `fill(root, data)` | Writes state to DOM via `data-ln-field`, `data-ln-attr`, `data-ln-show`, `data-ln-class` |
224
+
225
+ ### Pattern
226
+
227
+ ```javascript
228
+ import { createBatcher, fill, dispatch } from '../ln-core';
229
+
230
+ function _component(dom) {
231
+ this.dom = dom;
232
+ const self = this;
233
+
234
+ this.state = { key: null, value: null };
235
+
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
+ );
248
+ }
249
+
250
+ _component.prototype._render = function () {
251
+ fill(this.dom, this.state);
252
+ // additional DOM logic derived from state
253
+ };
254
+
255
+ _component.prototype._afterRender = function () {
256
+ dispatch(this.dom, 'ln-component:changed', { key: this.state.key });
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
+ };
264
+ ```
265
+
266
+ When multiple properties change in the same sync block:
267
+
268
+ ```javascript
269
+ this.state.key = 'status';
270
+ this.state.value = 'active';
271
+ this.queueRender();
272
+ // โ†’ microtask fires โ†’ _render() once โ†’ _afterRender() once
273
+ ```
274
+
275
+ ### `fill()` โ€” limitations
276
+
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()`.
278
+
279
+ ### When to use
280
+
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.
282
+
283
+ **Don't use it for:**
284
+ - Single boolean state โ€” `this.isOpen = true; dom.classList.toggle(...)` is simpler
285
+ - Event-driven rendering โ€” components that receive data via `set-data` events (ln-table)
286
+ - One-shot renders โ€” clone template + fill once, no ongoing state
287
+
288
+ ### Used in
289
+
290
+ Used in `ln-search`, `ln-filter`, `ln-list`, and `ln-data-coordinator` for microtask render coalescing.
291
+
292
+ ---
293
+
290
294
  ## CustomEvent Communication
291
295
 
292
296
  Components do NOT know about each other. Communication ONLY via CustomEvent.
@@ -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) |
@@ -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,43 +555,28 @@ 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
590
-
591
- ---
592
-
593
- ## Reactive Rendering Pattern
594
-
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.
614
-
615
- 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.
619
-
620
- For the full decision matrix (`reactiveState` vs `deepReactive`, when
621
- to use this pattern at all, anti-patterns), see
622
- [Reactive Rendering Pattern in components/COMPONENTS.md](../../components/COMPONENTS.md#reactive-rendering-pattern).
558
+ - Pattern: used in `ln-search`, `ln-filter`, `ln-list`, `ln-data-coordinator` to debounce/coalesce state and render operations
623
559
 
624
560
  ---
625
561
 
562
+ ## Reactive Rendering Pattern
563
+
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:
571
+
572
+ The pattern is: **state mutation โ†’ microtask queue โ†’ one render**.
573
+ Never wire sync state updates directly to immediate DOM writes when multiple attributes or filter parameters can change simultaneously โ€” batching prevents layout thrashing.
574
+
575
+ For further details, see
576
+ [Reactive Rendering Pattern in components/COMPONENTS.md](../../components/COMPONENTS.md#reactive-rendering-pattern).
577
+
578
+ ---
579
+
626
580
  ## window-cache.js
627
581
 
628
582
  ### createWindowCache(config)
@@ -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';
@@ -1,62 +1,3 @@
1
- // โ”€โ”€โ”€ Shallow Reactive โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
-
3
- export function reactiveState(initial, onChange) {
4
- return new Proxy(Object.assign({}, initial), {
5
- set(target, prop, value) {
6
- const old = target[prop];
7
- if (old === value) return true;
8
- target[prop] = value;
9
- onChange(prop, value, old);
10
- return true;
11
- }
12
- });
13
- }
14
-
15
- // โ”€โ”€โ”€ Deep Reactive โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
16
-
17
- const PROXY_FLAG = Symbol('deepReactive');
18
-
19
- export function deepReactive(obj, onChange) {
20
- function wrap(target) {
21
- if (target === null || typeof target !== 'object') return target;
22
- if (target[PROXY_FLAG]) return target;
23
-
24
- // Recursively wrap existing nested objects/arrays
25
- const keys = Object.keys(target);
26
- for (let i = 0; i < keys.length; i++) {
27
- const val = target[keys[i]];
28
- if (val !== null && typeof val === 'object') {
29
- target[keys[i]] = wrap(val);
30
- }
31
- }
32
-
33
- return new Proxy(target, {
34
- get(t, prop) {
35
- if (prop === PROXY_FLAG) return true;
36
- return t[prop];
37
- },
38
- set(t, prop, value) {
39
- const old = t[prop];
40
- if (value !== null && typeof value === 'object') {
41
- value = wrap(value);
42
- }
43
- t[prop] = value;
44
- if (old !== value) onChange();
45
- return true;
46
- },
47
- deleteProperty(t, prop) {
48
- if (prop in t) {
49
- delete t[prop];
50
- onChange();
51
- }
52
- return true;
53
- }
54
- });
55
- }
56
-
57
- return wrap(obj);
58
- }
59
-
60
1
  // โ”€โ”€โ”€ Render Batcher โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
61
2
 
62
3
  export function createBatcher(renderFn, afterRender) {