@adaas/are-html 0.0.22 → 0.0.24

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 (191) hide show
  1. package/dist/browser/index.d.mts +194 -10
  2. package/dist/browser/index.mjs +696 -245
  3. package/dist/browser/index.mjs.map +1 -1
  4. package/dist/node/{AreBinding.attribute-doUvtOjc.d.mts → AreBinding.attribute-BWzEIw6H.d.mts} +45 -0
  5. package/dist/node/{AreBinding.attribute-Bm5LlOyE.d.ts → AreBinding.attribute-GpT-5Qmf.d.ts} +45 -0
  6. package/dist/node/attributes/AreBinding.attribute.d.mts +1 -1
  7. package/dist/node/attributes/AreBinding.attribute.d.ts +1 -1
  8. package/dist/node/attributes/AreDirective.attribute.d.mts +1 -1
  9. package/dist/node/attributes/AreDirective.attribute.d.ts +1 -1
  10. package/dist/node/attributes/AreEvent.attribute.d.mts +1 -1
  11. package/dist/node/attributes/AreEvent.attribute.d.ts +1 -1
  12. package/dist/node/attributes/AreStatic.attribute.d.mts +1 -1
  13. package/dist/node/attributes/AreStatic.attribute.d.ts +1 -1
  14. package/dist/node/directives/AreDirectiveFor.directive.d.mts +18 -1
  15. package/dist/node/directives/AreDirectiveFor.directive.d.ts +18 -1
  16. package/dist/node/directives/AreDirectiveFor.directive.js +57 -9
  17. package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
  18. package/dist/node/directives/AreDirectiveFor.directive.mjs +57 -9
  19. package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
  20. package/dist/node/directives/AreDirectiveIf.directive.d.mts +18 -2
  21. package/dist/node/directives/AreDirectiveIf.directive.d.ts +18 -2
  22. package/dist/node/directives/AreDirectiveIf.directive.js +29 -6
  23. package/dist/node/directives/AreDirectiveIf.directive.js.map +1 -1
  24. package/dist/node/directives/AreDirectiveIf.directive.mjs +29 -6
  25. package/dist/node/directives/AreDirectiveIf.directive.mjs.map +1 -1
  26. package/dist/node/directives/AreDirectiveShow.directive.d.mts +1 -1
  27. package/dist/node/directives/AreDirectiveShow.directive.d.ts +1 -1
  28. package/dist/node/engine/AreHTML.compiler.d.mts +4 -2
  29. package/dist/node/engine/AreHTML.compiler.d.ts +4 -2
  30. package/dist/node/engine/AreHTML.compiler.js +11 -4
  31. package/dist/node/engine/AreHTML.compiler.js.map +1 -1
  32. package/dist/node/engine/AreHTML.compiler.mjs +11 -4
  33. package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
  34. package/dist/node/engine/AreHTML.constants.d.mts +33 -1
  35. package/dist/node/engine/AreHTML.constants.d.ts +33 -1
  36. package/dist/node/engine/AreHTML.constants.js +166 -0
  37. package/dist/node/engine/AreHTML.constants.js.map +1 -1
  38. package/dist/node/engine/AreHTML.constants.mjs +165 -1
  39. package/dist/node/engine/AreHTML.constants.mjs.map +1 -1
  40. package/dist/node/engine/AreHTML.context.d.mts +66 -0
  41. package/dist/node/engine/AreHTML.context.d.ts +66 -0
  42. package/dist/node/engine/AreHTML.context.js +98 -0
  43. package/dist/node/engine/AreHTML.context.js.map +1 -1
  44. package/dist/node/engine/AreHTML.context.mjs +98 -0
  45. package/dist/node/engine/AreHTML.context.mjs.map +1 -1
  46. package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
  47. package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
  48. package/dist/node/engine/AreHTML.interpreter.js +66 -10
  49. package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
  50. package/dist/node/engine/AreHTML.interpreter.mjs +66 -10
  51. package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
  52. package/dist/node/engine/AreHTML.lifecycle.d.mts +1 -8
  53. package/dist/node/engine/AreHTML.lifecycle.d.ts +1 -8
  54. package/dist/node/engine/AreHTML.lifecycle.js +29 -44
  55. package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
  56. package/dist/node/engine/AreHTML.lifecycle.mjs +29 -44
  57. package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
  58. package/dist/node/engine/AreHTML.tokenizer.d.mts +1 -1
  59. package/dist/node/engine/AreHTML.tokenizer.d.ts +1 -1
  60. package/dist/node/engine/AreHTML.tokenizer.js +7 -1
  61. package/dist/node/engine/AreHTML.tokenizer.js.map +1 -1
  62. package/dist/node/engine/AreHTML.tokenizer.mjs +7 -1
  63. package/dist/node/engine/AreHTML.tokenizer.mjs.map +1 -1
  64. package/dist/node/engine/AreHTML.transformer.d.mts +1 -1
  65. package/dist/node/engine/AreHTML.transformer.d.ts +1 -1
  66. package/dist/node/index.d.mts +4 -3
  67. package/dist/node/index.d.ts +4 -3
  68. package/dist/node/index.js +7 -0
  69. package/dist/node/index.mjs +1 -0
  70. package/dist/node/instructions/AddStaticHTML.instruction.d.mts +8 -0
  71. package/dist/node/instructions/AddStaticHTML.instruction.d.ts +8 -0
  72. package/dist/node/instructions/AddStaticHTML.instruction.js +31 -0
  73. package/dist/node/instructions/AddStaticHTML.instruction.js.map +1 -0
  74. package/dist/node/instructions/AddStaticHTML.instruction.mjs +24 -0
  75. package/dist/node/instructions/AddStaticHTML.instruction.mjs.map +1 -0
  76. package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
  77. package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
  78. package/dist/node/instructions/AreHTML.instructions.constants.js +1 -0
  79. package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
  80. package/dist/node/instructions/AreHTML.instructions.constants.mjs +1 -0
  81. package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
  82. package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
  83. package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
  84. package/dist/node/lib/AreDirective/AreDirective.component.d.mts +1 -1
  85. package/dist/node/lib/AreDirective/AreDirective.component.d.ts +1 -1
  86. package/dist/node/lib/AreDirective/AreDirective.types.d.mts +1 -1
  87. package/dist/node/lib/AreDirective/AreDirective.types.d.ts +1 -1
  88. package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.mts +1 -1
  89. package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.ts +1 -1
  90. package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.mts +1 -1
  91. package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.ts +1 -1
  92. package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.mts +1 -1
  93. package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.ts +1 -1
  94. package/dist/node/lib/AreHTMLNode/AreHTMLNode.js +51 -0
  95. package/dist/node/lib/AreHTMLNode/AreHTMLNode.js.map +1 -1
  96. package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs +51 -0
  97. package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs.map +1 -1
  98. package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
  99. package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
  100. package/dist/node/nodes/AreComment.d.mts +1 -1
  101. package/dist/node/nodes/AreComment.d.ts +1 -1
  102. package/dist/node/nodes/AreComponent.d.mts +1 -1
  103. package/dist/node/nodes/AreComponent.d.ts +1 -1
  104. package/dist/node/nodes/AreInterpolation.d.mts +1 -1
  105. package/dist/node/nodes/AreInterpolation.d.ts +1 -1
  106. package/dist/node/nodes/AreRoot.d.mts +1 -1
  107. package/dist/node/nodes/AreRoot.d.ts +1 -1
  108. package/dist/node/nodes/AreText.d.mts +1 -1
  109. package/dist/node/nodes/AreText.d.ts +1 -1
  110. package/examples/dashboard/concept.ts +1 -1
  111. package/examples/dashboard/dist/index.html +1 -1
  112. package/examples/dashboard/dist/{mqh9ryml-xat335.js → mqiw5sqa-ypckmj.js} +403 -57
  113. package/examples/for-perf/dist/index.html +1 -1
  114. package/examples/for-perf/dist/{mqh9ryfo-6a8d0o.js → mqp8i2py-vltsx0.js} +3030 -2474
  115. package/examples/lazy-loading/README.md +76 -0
  116. package/examples/lazy-loading/concept.ts +55 -0
  117. package/examples/lazy-loading/containers/UI.container.ts +215 -0
  118. package/examples/lazy-loading/dist/app.js +3803 -0
  119. package/examples/{for-perf/dist/mqh9ryfq-4pf5cv.js → lazy-loading/dist/chunks/chunk-6K72IBO4.js} +2708 -5476
  120. package/examples/lazy-loading/dist/index.html +36 -0
  121. package/examples/lazy-loading/dist/lazy/about-page.js +59 -0
  122. package/examples/lazy-loading/dist/lazy/reports-page.js +65 -0
  123. package/examples/lazy-loading/dist/lazy/settings-page.js +54 -0
  124. package/examples/lazy-loading/public/index.html +36 -0
  125. package/examples/lazy-loading/src/components/AppShell.component.ts +44 -0
  126. package/examples/lazy-loading/src/components/HomePage.component.ts +59 -0
  127. package/examples/lazy-loading/src/components/LazyOutlet.component.ts +108 -0
  128. package/examples/lazy-loading/src/components/NavBar.component.ts +98 -0
  129. package/examples/lazy-loading/src/concept.ts +116 -0
  130. package/examples/lazy-loading/src/lazy/AboutPage.component.ts +54 -0
  131. package/examples/lazy-loading/src/lazy/ReportsPage.component.ts +56 -0
  132. package/examples/lazy-loading/src/lazy/SettingsPage.component.ts +45 -0
  133. package/examples/lazy-loading/src/runtime/ComponentManifest.fragment.ts +61 -0
  134. package/examples/lazy-loading/src/runtime/LazyComponentResolver.fragment.ts +77 -0
  135. package/examples/os-desktop/README.md +91 -0
  136. package/examples/os-desktop/concept.ts +54 -0
  137. package/examples/os-desktop/containers/OS.container.ts +198 -0
  138. package/examples/os-desktop/containers/apps/AppBackend.ts +29 -0
  139. package/examples/os-desktop/containers/apps/GanttApp.backend.ts +56 -0
  140. package/examples/os-desktop/containers/apps/MarketingApp.backend.ts +68 -0
  141. package/examples/os-desktop/dist/app.js +4410 -0
  142. package/examples/os-desktop/dist/apps/gantt/app.js +271 -0
  143. package/examples/os-desktop/dist/apps/marketing/app.js +346 -0
  144. package/examples/{for-perf/dist/mqh9ryde-m243t8.js → os-desktop/dist/chunks/chunk-6K72IBO4.js} +2708 -5476
  145. package/examples/os-desktop/dist/chunks/chunk-EIIGUL6N.js +30 -0
  146. package/examples/os-desktop/dist/chunks/chunk-WOH7L5UR.js +30 -0
  147. package/examples/os-desktop/dist/index.html +33 -0
  148. package/examples/os-desktop/public/index.html +33 -0
  149. package/examples/os-desktop/src/apps/gantt/GanttApp.component.ts +41 -0
  150. package/examples/os-desktop/src/apps/gantt/GanttChart.component.ts +126 -0
  151. package/examples/os-desktop/src/apps/gantt/GanttStore.ts +47 -0
  152. package/examples/os-desktop/src/apps/gantt/GanttToolbar.component.ts +73 -0
  153. package/examples/os-desktop/src/apps/gantt/index.ts +13 -0
  154. package/examples/os-desktop/src/apps/marketing/MarketingApp.component.ts +53 -0
  155. package/examples/os-desktop/src/apps/marketing/MarketingStore.ts +34 -0
  156. package/examples/os-desktop/src/apps/marketing/PostEditor.component.ts +153 -0
  157. package/examples/os-desktop/src/apps/marketing/PostPreview.component.ts +110 -0
  158. package/examples/os-desktop/src/apps/marketing/index.ts +16 -0
  159. package/examples/os-desktop/src/concept.ts +126 -0
  160. package/examples/os-desktop/src/os/AppStage.component.ts +112 -0
  161. package/examples/os-desktop/src/os/AppWindow.component.ts +102 -0
  162. package/examples/os-desktop/src/os/Desktop.component.ts +106 -0
  163. package/examples/os-desktop/src/os/Dock.component.ts +174 -0
  164. package/examples/os-desktop/src/os/Hud.component.ts +83 -0
  165. package/examples/os-desktop/src/os/Launchpad.component.ts +191 -0
  166. package/examples/os-desktop/src/os/MenuBar.component.ts +156 -0
  167. package/examples/os-desktop/src/runtime/AppComponentResolver.fragment.ts +121 -0
  168. package/examples/os-desktop/src/runtime/AppRegistry.fragment.ts +104 -0
  169. package/examples/os-desktop/src/signals/MouseState.signal.ts +34 -0
  170. package/examples/os-desktop/src/signals/OSRoute.signal.ts +37 -0
  171. package/examples/os-desktop/src/signals/SelectionState.signal.ts +34 -0
  172. package/examples/signal-routing/dist/index.html +1 -1
  173. package/examples/signal-routing/dist/{mqh9ryc9-dkcbkx.js → mqp8hgce-4d6rh0.js} +3196 -2640
  174. package/package.json +13 -9
  175. package/src/directives/AreDirectiveFor.directive.ts +99 -16
  176. package/src/directives/AreDirectiveIf.directive.ts +33 -4
  177. package/src/engine/AreHTML.compiler.ts +25 -2
  178. package/src/engine/AreHTML.constants.ts +142 -0
  179. package/src/engine/AreHTML.context.ts +112 -0
  180. package/src/engine/AreHTML.interpreter.ts +114 -13
  181. package/src/engine/AreHTML.lifecycle.ts +81 -74
  182. package/src/engine/AreHTML.tokenizer.ts +30 -1
  183. package/src/index.ts +1 -0
  184. package/src/instructions/AddStaticHTML.instruction.ts +23 -0
  185. package/src/instructions/AreHTML.instructions.constants.ts +1 -0
  186. package/src/instructions/AreHTML.instructions.types.ts +9 -0
  187. package/src/lib/AreHTMLNode/AreHTMLNode.ts +74 -0
  188. package/src/lib/AreRoot/AreRoot.component.ts +3 -3
  189. package/tests/PropPropagation.test.ts +181 -0
  190. package/tests/StaticIsland.test.ts +115 -0
  191. package/tests/jest.setup.ts +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaas/are-html",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "A-Concept Rendering Engine (ARE) is a powerful rendering engine designed to work seamlessly with the A-Concept framework. This library provides an HTML engine implementation of ARE, enabling developers to create dynamic and interactive user interfaces for web applications using standard HTML syntax.",
5
5
  "keywords": [
6
6
  "a-concept",
@@ -71,6 +71,8 @@
71
71
  "example:jumpstart": "nodemon ./examples/jumpstart/concept.ts",
72
72
  "example:dashboard": "nodemon ./examples/dashboard/concept.ts",
73
73
  "example:signal-routing": "nodemon ./examples/signal-routing/concept.ts",
74
+ "example:lazy-loading": "nodemon ./examples/lazy-loading/concept.ts",
75
+ "example:os-desktop": "nodemon ./examples/os-desktop/concept.ts",
74
76
  "example:component-styles": "nodemon ./examples/component-styles/concept.ts",
75
77
  "example:auxta": "nodemon ./examples/auxta/concept.ts",
76
78
  "example:for-perf": "nodemon ./examples/for-perf/concept.ts",
@@ -83,16 +85,16 @@
83
85
  "build": "tsup --config tsup.config.ts"
84
86
  },
85
87
  "peerDependencies": {
86
- "@adaas/a-concept": "^0.3.29",
87
- "@adaas/a-frame": "^0.1.17",
88
- "@adaas/a-utils": "^0.3.34",
89
- "@adaas/are": "^0.0.23"
88
+ "@adaas/a-concept": "^0.3.30",
89
+ "@adaas/a-frame": "^0.1.18",
90
+ "@adaas/a-utils": "^0.3.35",
91
+ "@adaas/are": "^0.0.25"
90
92
  },
91
93
  "devDependencies": {
92
- "@adaas/a-concept": "^0.3.29",
93
- "@adaas/a-frame": "^0.1.17",
94
- "@adaas/a-utils": "^0.3.34",
95
- "@adaas/are": "^0.0.23",
94
+ "@adaas/a-concept": "^0.3.30",
95
+ "@adaas/a-frame": "^0.1.18",
96
+ "@adaas/a-utils": "^0.3.35",
97
+ "@adaas/are": "^0.0.25",
96
98
  "@types/chai": "^4.3.14",
97
99
  "@types/jest": "^29.5.12",
98
100
  "@types/mocha": "^10.0.6",
@@ -101,6 +103,8 @@
101
103
  "chai": "^5.1.0",
102
104
  "dotenv": "^16.4.5",
103
105
  "jest": "^29.7.0",
106
+ "jest-environment-jsdom": "^29.7.0",
107
+ "jsdom": "^29.1.1",
104
108
  "mocha": "^10.4.0",
105
109
  "nodemon": "^3.1.4",
106
110
  "ts-jest": "^29.1.2",
@@ -8,6 +8,7 @@ import { AreHTMLNode } from "@adaas/are-html/node";
8
8
  import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
9
9
  import { A_Frame } from "@adaas/a-frame/core";
10
10
  import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
11
+ import { AreHTMLEngineContext } from "@adaas/are-html/context";
11
12
 
12
13
 
13
14
  type AreForExpression = {
@@ -109,7 +110,12 @@ export class AreDirectiveFor extends AreDirective {
109
110
  * Parse the $for expression and evaluate the source array.
110
111
  */
111
112
  const { key, index, arrayExpr } = this.parseExpression(attribute.content);
112
- const array = this.resolveArray(store, arrayExpr, attribute.content);
113
+ // Item-scoped variables from an enclosing directive (e.g. the `row` of an
114
+ // outer `$for`) so a nested `$for="cell in row.cells"` resolves correctly.
115
+ // Use resolve() (not resolveFlat) so the ENCLOSING item's context — which
116
+ // lives on an ancestor scope, not on this directive's own node — is found.
117
+ const contextScope = attribute.owner.scope.resolve(AreDirectiveContext)?.scope || {};
118
+ const array = this.resolveArray(store, arrayExpr, attribute.content, contextScope);
113
119
 
114
120
  attribute.value = array;
115
121
 
@@ -193,9 +199,12 @@ export class AreDirectiveFor extends AreDirective {
193
199
  * Re-evaluate the source array.
194
200
  */
195
201
  const { key, index, arrayExpr, trackExpr } = this.parseExpression(attribute.content);
196
- const newArray = this.resolveArray(store, arrayExpr, attribute.content);
197
-
198
202
  const owner = attribute.owner;
203
+ // Item-scoped variables from an enclosing directive (see transform()).
204
+ // resolve() walks ancestor scopes to find the enclosing item's context.
205
+ const contextScope = owner.scope.resolve(AreDirectiveContext)?.scope || {};
206
+ const newArray = this.resolveArray(store, arrayExpr, attribute.content, contextScope);
207
+
199
208
  const currentChildren = [...owner.children] as AreHTMLNode[];
200
209
 
201
210
  attribute.value = newArray;
@@ -239,15 +248,25 @@ export class AreDirectiveFor extends AreDirective {
239
248
  // render, so it is deferred into the time-sliced loop below alongside
240
249
  // transform/compile/mount. Existing (keyed) children are reconciled in
241
250
  // place synchronously — that is cheap and keeps reused rows stable.
242
- const toCreate: Array<{ item: any; idx: number }> = [];
251
+ const toCreate: Array<{ item: any; idx: number; key: any }> = [];
252
+
253
+ // Final identity → node map covering BOTH reused and newly created item
254
+ // nodes, plus the desired key order. After all items are mounted these
255
+ // drive the DOM reorder pass (step 5) so the rendered order always
256
+ // matches the source array — making prepend / shuffle / arbitrary
257
+ // reorders move existing rows instead of only appending at the end.
258
+ const finalByKey = new Map<any, AreHTMLNode>();
259
+ const orderedKeys: any[] = new Array(newArray.length);
243
260
 
244
261
  for (let i = 0; i < newArray.length; i++) {
245
262
  const item = newArray[i];
246
263
  const k = computeKey(item, i);
264
+ orderedKeys[i] = k;
247
265
  const existing = childByKey.get(k);
248
266
 
249
267
  if (existing) {
250
268
  remaining.delete(existing);
269
+ finalByKey.set(k, existing);
251
270
 
252
271
  let directiveContext = existing.scope.resolveFlat(AreDirectiveContext);
253
272
  if (!directiveContext) {
@@ -260,7 +279,7 @@ export class AreDirectiveFor extends AreDirective {
260
279
  [index || 'index']: i,
261
280
  };
262
281
  } else {
263
- toCreate.push({ item, idx: i });
282
+ toCreate.push({ item, idx: i, key: k });
264
283
  }
265
284
  }
266
285
 
@@ -274,12 +293,13 @@ export class AreDirectiveFor extends AreDirective {
274
293
  }
275
294
 
276
295
  // ── 4. Create + mount the new item nodes. ───────────────────────────
277
- // `spawnItemNode` appends to `owner.children` immediately, so iterating
278
- // `toCreate` in source order preserves list order (reused children keep
279
- // their positions, new rows are appended in order) identical to the
280
- // previous synchronous behavior.
281
- const createItem = (desc: { item: any; idx: number }) => {
296
+ // `spawnItemNode` appends to `owner.children` immediately; new rows are
297
+ // therefore mounted at the end (just before the anchor comment). The
298
+ // reorder pass (step 5) then moves any out-of-position node so the final
299
+ // DOM order matches the source array.
300
+ const createItem = (desc: { item: any; idx: number; key: any }) => {
282
301
  const child = this.spawnItemNode(attribute.template!, owner, key, index, desc.item, desc.idx);
302
+ finalByKey.set(desc.key, child);
283
303
  child.transform();
284
304
  child.compile();
285
305
  // While detached, stop after compile: the item's instructions are
@@ -292,6 +312,8 @@ export class AreDirectiveFor extends AreDirective {
292
312
  // Small lists → fully synchronous, identical to the previous behavior.
293
313
  if (toCreate.length <= AreDirectiveFor.SYNC_THRESHOLD) {
294
314
  for (const desc of toCreate) createItem(desc);
315
+ // ── 5. Reorder live DOM to match the source array order ──────────
316
+ if (attached) this.reconcileOrder(owner, orderedKeys, finalByKey);
295
317
  return this.finishUpdate(attribute, store, scene, state);
296
318
  }
297
319
 
@@ -325,12 +347,55 @@ export class AreDirectiveFor extends AreDirective {
325
347
  });
326
348
  }
327
349
 
350
+ // ── 5. Reorder live DOM to match the source array order ──────────
351
+ if (attached) this.reconcileOrder(owner, orderedKeys, finalByKey);
328
352
  return this.finishUpdate(attribute, store, scene, state);
329
353
  };
330
354
 
331
355
  return processChunk();
332
356
  }
333
357
 
358
+ /**
359
+ * Repositions the item nodes' DOM elements so the rendered order matches the
360
+ * source array order. The keyed diff (steps 1–4) reuses existing nodes in
361
+ * place and mounts new ones at the end; without this pass a `prepend` or
362
+ * `shuffle` would leave reused rows where they were and pile new rows at the
363
+ * bottom. We walk the desired order RIGHT-TO-LEFT, keeping a `ref` pointer to
364
+ * the element each item must precede (starting at the `$for` anchor comment),
365
+ * and only call `insertBefore` when an element is not already in position —
366
+ * so a plain `append` (already-correct order) performs ZERO DOM moves.
367
+ */
368
+ private reconcileOrder(
369
+ owner: AreHTMLNode,
370
+ orderedKeys: any[],
371
+ finalByKey: Map<any, AreHTMLNode>,
372
+ ): void {
373
+ const context = owner.scope.resolve<AreHTMLEngineContext>(AreHTMLEngineContext);
374
+ if (!context) return;
375
+
376
+ const anchor = context.getNodeElement(owner);
377
+ if (!anchor || !anchor.parentNode) return;
378
+
379
+ const parent = anchor.parentNode;
380
+ let ref: Node = anchor;
381
+
382
+ for (let i = orderedKeys.length - 1; i >= 0; i--) {
383
+ const node = finalByKey.get(orderedKeys[i]);
384
+ if (!node) continue;
385
+
386
+ const element = context.getNodeElement(node);
387
+ // Element may be missing if the item is still detached/unmounted.
388
+ if (!element || element.parentNode !== parent) continue;
389
+
390
+ // Already immediately before `ref` — no move needed.
391
+ if (element.nextSibling !== ref) {
392
+ parent.insertBefore(element, ref);
393
+ }
394
+ ref = element;
395
+ }
396
+ }
397
+
398
+
334
399
  /**
335
400
  * Completes an update pass. If another update() arrived while a chunked
336
401
  * render was streaming, run exactly one more pass now from the latest store
@@ -441,14 +506,32 @@ export class AreDirectiveFor extends AreDirective {
441
506
  * Supports both plain key lookups and function-call expressions:
442
507
  * items → store.get('items')
443
508
  * filter(items) → store.get('filter')(store.get('items'))
509
+ *
510
+ * `contextScope` carries item-scoped variables introduced by an enclosing
511
+ * directive (e.g. the `row` of an outer `$for`). It is consulted BEFORE the
512
+ * store so a nested `$for="cell in row.cells"` resolves `row` from the
513
+ * parent iteration instead of looking for a (non-existent) top-level store
514
+ * key. Leading identifiers not present in the context fall back to the store.
444
515
  */
445
- private resolveArray(store: AreStore, arrayExpr: string, fullContent: string): any[] {
516
+ private resolveArray(
517
+ store: AreStore,
518
+ arrayExpr: string,
519
+ fullContent: string,
520
+ contextScope: Record<string, any> = {},
521
+ ): any[] {
522
+ // Resolve a leading identifier from the directive context first, then
523
+ // the store — mirrors how bindings/interpolations evaluate scoped vars.
524
+ const getRoot = (rawKey: string): any => {
525
+ const k = rawKey.replace(/\?$/, '');
526
+ return (k in contextScope) ? contextScope[k] : store.get(k as any);
527
+ };
528
+
446
529
  let result: any;
447
530
  const callMatch = arrayExpr.match(/^([^(]+)\((.+)\)$/);
448
531
 
449
532
  if (callMatch) {
450
533
  const fnName = callMatch[1].trim();
451
- const fn = store.get(fnName as any);
534
+ const fn = getRoot(fnName);
452
535
 
453
536
  if (typeof fn !== 'function')
454
537
  throw new AreCompilerError({
@@ -465,14 +548,14 @@ export class AreDirectiveFor extends AreDirective {
465
548
  const stripped = arg.replace(/\?$/, '');
466
549
  if (stripped.includes('.')) {
467
550
  const parts = stripped.split('.').map(p => p.replace(/\?$/, ''));
468
- let val: any = store.get(parts[0] as any);
551
+ let val: any = getRoot(parts[0]);
469
552
  for (let j = 1; j < parts.length; j++) {
470
553
  if (val == null) return undefined;
471
554
  val = val[parts[j]];
472
555
  }
473
556
  return val ?? undefined;
474
557
  }
475
- return store.get(stripped as any);
558
+ return getRoot(stripped);
476
559
  });
477
560
 
478
561
  result = (fn as Function)(...resolvedArgs);
@@ -481,13 +564,13 @@ export class AreDirectiveFor extends AreDirective {
481
564
  // Strip optional-chaining `?` suffix from each segment so that
482
565
  // `record?.keywords` resolves the same as `record.keywords`.
483
566
  const parts = arrayExpr.split('.').map(p => p.replace(/\?$/, ''));
484
- result = store.get(parts[0] as any);
567
+ result = getRoot(parts[0]);
485
568
  for (let i = 1; i < parts.length; i++) {
486
569
  if (result == null) break;
487
570
  result = result[parts[i]];
488
571
  }
489
572
  } else {
490
- result = store.get(arrayExpr.replace(/\?$/, '') as any);
573
+ result = getRoot(arrayExpr);
491
574
  }
492
575
 
493
576
  // null / undefined from optional-chaining expressions (e.g. `record?.keywords`)
@@ -96,9 +96,7 @@ export class AreDirectiveIf extends AreDirective {
96
96
  * 1. Extract the value from the store based on the attribute content
97
97
  * (which is the path to the value in the store)
98
98
  */
99
- attribute.value = syntax.evaluate(attribute.content, store, {
100
- ...(directiveContext?.scope || {}),
101
- });
99
+ attribute.value = this.evaluateCondition(syntax, attribute, store, directiveContext);
102
100
 
103
101
  /**
104
102
  * 2. If the value is falsy, remove the node from the scene by planning a RemoveElement instruction.
@@ -127,6 +125,7 @@ export class AreDirectiveIf extends AreDirective {
127
125
  @A_Inject(A_Scope) scope: A_Scope,
128
126
  @A_Inject(AreSyntax) syntax: AreSyntax,
129
127
  @A_Inject(AreScene) scene: AreScene,
128
+ @A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
130
129
  ...args: any[]
131
130
  ): void {
132
131
  /**
@@ -134,7 +133,7 @@ export class AreDirectiveIf extends AreDirective {
134
133
  * (which is the path to the value in the store)
135
134
  */
136
135
  const previous = !!attribute.value;
137
- const next = !!syntax.evaluate(attribute.content, store);
136
+ const next = this.evaluateCondition(syntax, attribute, store, directiveContext);
138
137
  attribute.value = next;
139
138
 
140
139
  // Skip when truthiness has not changed — avoids redundant mount/unmount.
@@ -149,4 +148,34 @@ export class AreDirectiveIf extends AreDirective {
149
148
  }
150
149
  }
151
150
 
151
+ /**
152
+ * Evaluates the `$if` condition defensively.
153
+ *
154
+ * A condition can reference data that is momentarily unavailable — most
155
+ * commonly a nested `$if` (e.g. `$if="selected.fields.length"`) living
156
+ * inside a parent `$if="selected"` whose object has just become `null`.
157
+ * Because the nested directive is still subscribed to the store, its
158
+ * update fires on that same change and the raw expression would throw
159
+ * `Cannot read properties of null`, crashing the whole update pipeline.
160
+ *
161
+ * Treating an evaluation error as `false` is the correct contract for a
162
+ * conditional: if the condition cannot be resolved, the subtree simply
163
+ * stays hidden until the referenced data is present again (at which point
164
+ * the parent `$if` re-activates and re-evaluates this one).
165
+ */
166
+ private evaluateCondition(
167
+ syntax: AreSyntax,
168
+ attribute: AreDirectiveAttribute,
169
+ store: AreStore,
170
+ directiveContext?: AreDirectiveContext,
171
+ ): boolean {
172
+ try {
173
+ return !!syntax.evaluate(attribute.content, store, {
174
+ ...(directiveContext?.scope || {}),
175
+ });
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
152
181
  }
@@ -13,7 +13,9 @@ import { AddAttributeInstruction} from "@adaas/are-html/instructions/AddAttribut
13
13
  import { AddTextInstruction} from "@adaas/are-html/instructions/AddText.instruction";
14
14
  import { AddListenerInstruction} from "@adaas/are-html/instructions/AddListener.instruction";
15
15
  import { AddStyleInstruction } from "@adaas/are-html/instructions/AddStyle.instruction";
16
+ import { AddStaticHTMLInstruction } from "@adaas/are-html/instructions/AddStaticHTML.instruction";
16
17
  import { AreHTMLNode } from "@adaas/are-html/node";
18
+ import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
17
19
 
18
20
 
19
21
 
@@ -39,6 +41,18 @@ export class AreHTMLCompiler extends AreCompiler {
39
41
  ): void {
40
42
  super.compile(node, scene, logger, ...args);
41
43
 
44
+ /**
45
+ * Static-island materialisation. When the tokenizer flagged this node as
46
+ * a static island its inner subtree was never exploded into child nodes,
47
+ * so there is nothing for the base compiler to walk. Emit a single
48
+ * AddStaticHTML instruction carrying the captured inner markup; the
49
+ * interpreter injects it onto the host element in one pass (and decodes
50
+ * HTML entities for free).
51
+ */
52
+ if (node.isStaticIsland && scene.host) {
53
+ scene.plan(new AddStaticHTMLInstruction(scene.host, { html: node.staticInnerHTML! }));
54
+ }
55
+
42
56
  if (node.styles?.styles) {
43
57
  const host = scene.host;
44
58
  if (host) {
@@ -206,6 +220,7 @@ export class AreHTMLCompiler extends AreCompiler {
206
220
  @A_Inject(AreStore) parentStore: AreStore,
207
221
  @A_Inject(AreStore) store: AreStore,
208
222
  @A_Inject(AreSyntax) syntax: AreSyntax,
223
+ @A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
209
224
  ...args: any[]
210
225
  ) {
211
226
  if (!scene.host)
@@ -249,13 +264,21 @@ export class AreHTMLCompiler extends AreCompiler {
249
264
  return value;
250
265
  };
251
266
 
267
+ // Item-scoped variables from an enclosing directive (the `item`/`index`
268
+ // of a `$for`, or any scope a `$if` merged in) so a prop binding like
269
+ // `:title="item.name"` resolves the loop variable — checked BEFORE the
270
+ // store, exactly like plain attribute bindings do in the interpreter.
271
+ // Read `.scope` lazily inside each evaluation so keyed `$for` updates
272
+ // that reassign the context's scope are always reflected.
273
+ const directiveScope = () => directiveContext?.scope ?? {};
274
+
252
275
  // The watcher entity below is registered against parentStore so that
253
276
  // updates to the bound expression in the parent flow into the child store.
254
277
  const watcher = {
255
278
  update: () => {
256
279
  try {
257
280
  parentStore.watch(watcher);
258
- const next = coerce(syntax.evaluate(attribute.content, parentStore));
281
+ const next = coerce(syntax.evaluate(attribute.content, parentStore, directiveScope()));
259
282
  parentStore.unwatch(watcher);
260
283
  store.set(propName!, next);
261
284
  } catch (e) {
@@ -266,7 +289,7 @@ export class AreHTMLCompiler extends AreCompiler {
266
289
 
267
290
  // Initial read with watch active so dependencies are recorded.
268
291
  parentStore.watch(watcher);
269
- const initial = coerce(syntax.evaluate(attribute.content, parentStore));
292
+ const initial = coerce(syntax.evaluate(attribute.content, parentStore, directiveScope()));
270
293
  parentStore.unwatch(watcher);
271
294
 
272
295
  store.set(propName, initial);
@@ -181,3 +181,145 @@ export function toDOMString(value: any): string {
181
181
  }
182
182
 
183
183
 
184
+ // ─────────────────────────────────────────────────────────────────────────────
185
+ // ── Static-island detection ──────────────────────────────────────────────────
186
+ // ─────────────────────────────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Standard HTML element names that are safe to materialise wholesale via
190
+ * `innerHTML` / a cached `<template>` clone.
191
+ *
192
+ * The set is intentionally an allow-list of plain HTML flow/phrasing/table/list
193
+ * /form-display tags. Anything NOT in this set — custom elements, registered
194
+ * ARE components (resolved by PascalCase tag), and SVG/MathML elements — is
195
+ * excluded so those subtrees keep flowing through the normal per-node pipeline
196
+ * (SVG needs createElementNS; components need their own lifecycle).
197
+ */
198
+ export const STANDARD_HTML_TAGS = new Set<string>([
199
+ // root / sections
200
+ 'html', 'body', 'header', 'footer', 'main', 'nav', 'section', 'article',
201
+ 'aside', 'address', 'hgroup',
202
+ // headings
203
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
204
+ // grouping
205
+ 'div', 'p', 'span', 'pre', 'blockquote', 'figure', 'figcaption',
206
+ 'hr', 'br', 'wbr',
207
+ // lists
208
+ 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'menu',
209
+ // text-level / phrasing
210
+ 'a', 'b', 'i', 'u', 's', 'em', 'strong', 'small', 'mark', 'abbr', 'cite',
211
+ 'q', 'code', 'kbd', 'samp', 'var', 'sub', 'sup', 'time', 'data', 'dfn',
212
+ 'bdi', 'bdo', 'ruby', 'rt', 'rp', 'del', 'ins',
213
+ // media / embedded (no special namespace handling needed)
214
+ 'img', 'picture', 'source', 'figure', 'audio', 'video', 'track',
215
+ // tables
216
+ 'table', 'caption', 'colgroup', 'col', 'thead', 'tbody', 'tfoot',
217
+ 'tr', 'th', 'td',
218
+ // forms (display only — these still render fine from innerHTML)
219
+ 'label', 'fieldset', 'legend', 'datalist', 'option', 'optgroup', 'output',
220
+ 'progress', 'meter',
221
+ // interactive
222
+ 'details', 'summary', 'dialog',
223
+ ]);
224
+
225
+ /**
226
+ * Detects whether an inner-markup string is a fully *static island* — i.e. it
227
+ * contains no ARE-reactive constructs and therefore can be rendered in one shot
228
+ * (browser-parsed `innerHTML` / cached `<template>` clone) instead of being
229
+ * exploded into one AreNode per element/text/interpolation.
230
+ *
231
+ * A subtree is static iff it contains:
232
+ * 1. no `{{ }}` interpolations, and
233
+ * 2. no dynamic attributes (`$`-directive / `:`-binding / `@`-event), and
234
+ * 3. only standard HTML tags (no custom elements, ARE components or SVG).
235
+ *
236
+ * The scanner is quote-aware so a `:` / `@` / `$` inside an attribute *value*
237
+ * (e.g. `href="http://…"`, `style="color:red"`) is never mistaken for a
238
+ * dynamic-attribute prefix. The detector is deliberately conservative: any
239
+ * ambiguity resolves to `false` (skip the optimisation, keep the safe path).
240
+ *
241
+ * NOTE: pure-text content (no tags at all) is also considered static — this is
242
+ * what lets `&nbsp;`, `&amp;`, `&#160;` and friends decode correctly, since the
243
+ * browser HTML parser handles entities that hand-built text nodes do not.
244
+ */
245
+ export function isStaticMarkup(inner: string): boolean {
246
+ if (!inner) return false;
247
+ // 1. interpolations make the subtree dynamic
248
+ if (inner.indexOf('{{') !== -1) return false;
249
+
250
+ const n = inner.length;
251
+ let i = 0;
252
+
253
+ while (i < n) {
254
+ const lt = inner.indexOf('<', i);
255
+ if (lt === -1) break; // remaining content is plain text — safe
256
+
257
+ // HTML comment — inert, skip over it
258
+ if (inner.startsWith('<!--', lt)) {
259
+ const end = inner.indexOf('-->', lt + 4);
260
+ if (end === -1) return false; // malformed
261
+ i = end + 3;
262
+ continue;
263
+ }
264
+
265
+ // closing tag, doctype or processing instruction — skip to its '>'
266
+ if (inner[lt + 1] === '/' || inner[lt + 1] === '!' || inner[lt + 1] === '?') {
267
+ const gt = inner.indexOf('>', lt);
268
+ if (gt === -1) return false;
269
+ i = gt + 1;
270
+ continue;
271
+ }
272
+
273
+ // opening tag — extract the tag name
274
+ const nameMatch = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(inner.slice(lt));
275
+ if (!nameMatch) { i = lt + 1; continue; }
276
+
277
+ const tag = nameMatch[1].toLowerCase();
278
+ // custom element / ARE component / non-standard (incl. SVG) → not static
279
+ if (tag.indexOf('-') !== -1 || !STANDARD_HTML_TAGS.has(tag)) return false;
280
+
281
+ // walk the opening tag (quote-aware) to find its closing '>' and inspect
282
+ // attribute-name boundaries for dynamic prefixes
283
+ let j = lt + nameMatch[0].length;
284
+ let inSingle = false;
285
+ let inDouble = false;
286
+ let atNameBoundary = true; // true right after whitespace / '/' inside a tag
287
+ let tagEnd = -1;
288
+
289
+ while (j < n) {
290
+ const ch = inner[j];
291
+
292
+ if (inDouble) {
293
+ if (ch === '"') inDouble = false;
294
+ } else if (inSingle) {
295
+ if (ch === "'") inSingle = false;
296
+ } else if (ch === '"') {
297
+ inDouble = true;
298
+ atNameBoundary = false;
299
+ } else if (ch === "'") {
300
+ inSingle = true;
301
+ atNameBoundary = false;
302
+ } else if (ch === '>') {
303
+ tagEnd = j;
304
+ break;
305
+ } else if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '/') {
306
+ atNameBoundary = true;
307
+ } else {
308
+ // a dynamic-attribute prefix only counts when it STARTS an
309
+ // attribute name (i.e. sits at a name boundary, outside quotes)
310
+ if (atNameBoundary && (ch === '$' || ch === ':' || ch === '@')) {
311
+ return false;
312
+ }
313
+ atNameBoundary = false;
314
+ }
315
+ j++;
316
+ }
317
+
318
+ if (tagEnd === -1) return false; // unterminated tag — bail to safe path
319
+ i = tagEnd + 1;
320
+ }
321
+
322
+ return true;
323
+ }
324
+
325
+