lego-dom 0.0.8 → 1.0.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 (175) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +49 -432
  3. package/cdn.html +124 -0
  4. package/docs/.vitepress/config.js +43 -5
  5. package/docs/api/directives.md +3 -3
  6. package/docs/api/globals.md +1 -1
  7. package/docs/api/index.md +3 -3
  8. package/docs/api/vite-plugin.md +1 -1
  9. package/docs/contributing/01-welcome.md +36 -0
  10. package/docs/contributing/02-registry.md +99 -0
  11. package/docs/contributing/03-batcher.md +110 -0
  12. package/docs/contributing/04-reactivity.md +87 -0
  13. package/docs/contributing/05-caching.md +59 -0
  14. package/docs/contributing/06-init.md +125 -0
  15. package/docs/contributing/07-observer.md +69 -0
  16. package/docs/contributing/08-snap.md +126 -0
  17. package/docs/contributing/09-diffing.md +69 -0
  18. package/docs/contributing/10-studs.md +76 -0
  19. package/docs/contributing/11-scanner.md +104 -0
  20. package/docs/contributing/12-render.md +116 -0
  21. package/docs/contributing/13-directives.md +225 -0
  22. package/docs/contributing/14-events.md +57 -0
  23. package/docs/contributing/15-router.md +9 -0
  24. package/docs/contributing/16-state.md +48 -0
  25. package/docs/contributing/17-legodom.md +55 -0
  26. package/docs/contributing/index.md +5 -0
  27. package/docs/examples/form.md +2 -2
  28. package/docs/examples/index.md +4 -4
  29. package/docs/examples/routing.md +8 -8
  30. package/docs/examples/sfc-showcase.md +4 -4
  31. package/docs/examples/todo-app.md +3 -3
  32. package/docs/guide/cdn-usage.md +16 -8
  33. package/docs/guide/components.md +34 -16
  34. package/docs/guide/contributing.md +2 -2
  35. package/docs/guide/directives.md +23 -23
  36. package/docs/guide/getting-started.md +41 -16
  37. package/docs/guide/index.md +12 -12
  38. package/docs/guide/lifecycle.md +1 -1
  39. package/docs/guide/quick-start.md +8 -5
  40. package/docs/guide/reactivity.md +30 -9
  41. package/docs/guide/routing.md +189 -289
  42. package/docs/guide/sfc.md +40 -40
  43. package/docs/guide/templating.md +4 -4
  44. package/docs/index.md +48 -14
  45. package/docs/public/logo.svg +17 -38
  46. package/docs/router/basic-routing.md +103 -0
  47. package/docs/router/cold-entry.md +91 -0
  48. package/docs/router/history.md +69 -0
  49. package/docs/router/index.md +73 -0
  50. package/docs/router/resolver.md +74 -0
  51. package/docs/router/surgical-swaps.md +134 -0
  52. package/examples/vite-app/README.md +2 -2
  53. package/examples/vite-app/index.html +9 -13
  54. package/examples/vite-app/package.json +4 -2
  55. package/examples/vite-app/src/app.css +3 -0
  56. package/examples/vite-app/src/app.js +29 -0
  57. package/examples/vite-app/src/components/app-navbar.lego +34 -0
  58. package/examples/vite-app/src/components/customers/customer-details.lego +24 -0
  59. package/examples/vite-app/src/components/customers/customer-orders.lego +21 -0
  60. package/examples/vite-app/src/components/customers/order-list.lego +55 -0
  61. package/examples/vite-app/src/components/greeting-card.lego +26 -26
  62. package/examples/vite-app/src/components/sample-component.lego +58 -58
  63. package/examples/vite-app/src/components/shells/customers-shell.lego +21 -0
  64. package/examples/vite-app/src/components/todo-list.lego +239 -0
  65. package/examples/vite-app/src/components/widgets/user-card.lego +27 -0
  66. package/examples/vite-app/vite.config.js +7 -2
  67. package/lego.js +2 -0
  68. package/main.js +280 -83
  69. package/package.json +8 -3
  70. package/parse-lego.js +17 -8
  71. package/parse-lego.test.js +1 -1
  72. package/{main.test.js → tests/main.test.js} +34 -17
  73. package/tests/parse-lego.test.js +65 -0
  74. package/vite-plugin.js +62 -24
  75. package/docs/.vitepress/dist/404.html +0 -22
  76. package/docs/.vitepress/dist/api/define.html +0 -35
  77. package/docs/.vitepress/dist/api/directives.html +0 -32
  78. package/docs/.vitepress/dist/api/globals.html +0 -27
  79. package/docs/.vitepress/dist/api/index.html +0 -25
  80. package/docs/.vitepress/dist/api/lifecycle.html +0 -38
  81. package/docs/.vitepress/dist/api/route.html +0 -34
  82. package/docs/.vitepress/dist/api/vite-plugin.html +0 -37
  83. package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.js +0 -11
  84. package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.lean.js +0 -1
  85. package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.js +0 -8
  86. package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.lean.js +0 -1
  87. package/docs/.vitepress/dist/assets/api_globals.md.DOjt7AV0.js +0 -3
  88. package/docs/.vitepress/dist/assets/api_globals.md.DOjt7AV0.lean.js +0 -1
  89. package/docs/.vitepress/dist/assets/api_index.md.OS6h01ct.js +0 -1
  90. package/docs/.vitepress/dist/assets/api_index.md.OS6h01ct.lean.js +0 -1
  91. package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.js +0 -14
  92. package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.lean.js +0 -1
  93. package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.js +0 -10
  94. package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.lean.js +0 -1
  95. package/docs/.vitepress/dist/assets/api_vite-plugin.md.DNn9VhL5.js +0 -13
  96. package/docs/.vitepress/dist/assets/api_vite-plugin.md.DNn9VhL5.lean.js +0 -1
  97. package/docs/.vitepress/dist/assets/app.BG5s3B0P.js +0 -1
  98. package/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.DQmuWC2Z.js +0 -1
  99. package/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.BO-PSxt1.js +0 -9
  100. package/docs/.vitepress/dist/assets/chunks/framework.B7OFBR9X.js +0 -19
  101. package/docs/.vitepress/dist/assets/chunks/theme.DA-iSa9B.js +0 -2
  102. package/docs/.vitepress/dist/assets/examples_form.md.B3stGKbu.js +0 -34
  103. package/docs/.vitepress/dist/assets/examples_form.md.B3stGKbu.lean.js +0 -1
  104. package/docs/.vitepress/dist/assets/examples_index.md.BDEG_D4J.js +0 -30
  105. package/docs/.vitepress/dist/assets/examples_index.md.BDEG_D4J.lean.js +0 -1
  106. package/docs/.vitepress/dist/assets/examples_routing.md.bqZ9DjDK.js +0 -338
  107. package/docs/.vitepress/dist/assets/examples_routing.md.bqZ9DjDK.lean.js +0 -1
  108. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DLXaUiop.js +0 -13
  109. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DLXaUiop.lean.js +0 -1
  110. package/docs/.vitepress/dist/assets/examples_todo-app.md.D5RhZoo5.js +0 -297
  111. package/docs/.vitepress/dist/assets/examples_todo-app.md.D5RhZoo5.lean.js +0 -1
  112. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CAjf03Lr.js +0 -182
  113. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CAjf03Lr.lean.js +0 -1
  114. package/docs/.vitepress/dist/assets/guide_components.md.BIFWF1Hc.js +0 -174
  115. package/docs/.vitepress/dist/assets/guide_components.md.BIFWF1Hc.lean.js +0 -1
  116. package/docs/.vitepress/dist/assets/guide_contributing.md.BgbUN-Mr.js +0 -1
  117. package/docs/.vitepress/dist/assets/guide_contributing.md.BgbUN-Mr.lean.js +0 -1
  118. package/docs/.vitepress/dist/assets/guide_directives.md.Bi3ynu1d.js +0 -140
  119. package/docs/.vitepress/dist/assets/guide_directives.md.Bi3ynu1d.lean.js +0 -1
  120. package/docs/.vitepress/dist/assets/guide_getting-started.md.2Nr1lp2z.js +0 -107
  121. package/docs/.vitepress/dist/assets/guide_getting-started.md.2Nr1lp2z.lean.js +0 -1
  122. package/docs/.vitepress/dist/assets/guide_index.md.GvZq_Yf2.js +0 -2
  123. package/docs/.vitepress/dist/assets/guide_index.md.GvZq_Yf2.lean.js +0 -1
  124. package/docs/.vitepress/dist/assets/guide_lifecycle.md.B28j1OzS.js +0 -304
  125. package/docs/.vitepress/dist/assets/guide_lifecycle.md.B28j1OzS.lean.js +0 -1
  126. package/docs/.vitepress/dist/assets/guide_quick-start.md.CNk3VGTF.js +0 -33
  127. package/docs/.vitepress/dist/assets/guide_quick-start.md.CNk3VGTF.lean.js +0 -1
  128. package/docs/.vitepress/dist/assets/guide_reactivity.md.CVsaMaPv.js +0 -135
  129. package/docs/.vitepress/dist/assets/guide_reactivity.md.CVsaMaPv.lean.js +0 -1
  130. package/docs/.vitepress/dist/assets/guide_routing.md.DSpDP25o.js +0 -193
  131. package/docs/.vitepress/dist/assets/guide_routing.md.DSpDP25o.lean.js +0 -1
  132. package/docs/.vitepress/dist/assets/guide_sfc.md.CVUP66tS.js +0 -187
  133. package/docs/.vitepress/dist/assets/guide_sfc.md.CVUP66tS.lean.js +0 -1
  134. package/docs/.vitepress/dist/assets/guide_templating.md.BgCGe4aa.js +0 -119
  135. package/docs/.vitepress/dist/assets/guide_templating.md.BgCGe4aa.lean.js +0 -1
  136. package/docs/.vitepress/dist/assets/index.md.xV1taCED.js +0 -23
  137. package/docs/.vitepress/dist/assets/index.md.xV1taCED.lean.js +0 -1
  138. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  139. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  140. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  141. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  142. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  143. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  144. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  145. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  146. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  147. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  148. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  149. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  150. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  151. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  152. package/docs/.vitepress/dist/assets/style.eycE2Jhw.css +0 -1
  153. package/docs/.vitepress/dist/examples/form.html +0 -58
  154. package/docs/.vitepress/dist/examples/index.html +0 -368
  155. package/docs/.vitepress/dist/examples/routing.html +0 -362
  156. package/docs/.vitepress/dist/examples/sfc-showcase.html +0 -37
  157. package/docs/.vitepress/dist/examples/todo-app.html +0 -321
  158. package/docs/.vitepress/dist/guide/cdn-usage.html +0 -206
  159. package/docs/.vitepress/dist/guide/components.html +0 -198
  160. package/docs/.vitepress/dist/guide/contributing.html +0 -25
  161. package/docs/.vitepress/dist/guide/directives.html +0 -164
  162. package/docs/.vitepress/dist/guide/getting-started.html +0 -131
  163. package/docs/.vitepress/dist/guide/index.html +0 -26
  164. package/docs/.vitepress/dist/guide/lifecycle.html +0 -328
  165. package/docs/.vitepress/dist/guide/quick-start.html +0 -57
  166. package/docs/.vitepress/dist/guide/reactivity.html +0 -159
  167. package/docs/.vitepress/dist/guide/routing.html +0 -217
  168. package/docs/.vitepress/dist/guide/sfc.html +0 -211
  169. package/docs/.vitepress/dist/guide/templating.html +0 -143
  170. package/docs/.vitepress/dist/hashmap.json +0 -1
  171. package/docs/.vitepress/dist/index.html +0 -47
  172. package/docs/.vitepress/dist/logo.svg +0 -38
  173. package/docs/.vitepress/dist/vp-icons.css +0 -1
  174. package/examples/vite-app/src/main.js +0 -11
  175. package/examples.js +0 -99
package/main.js CHANGED
@@ -1,9 +1,14 @@
1
1
  const Lego = (() => {
2
2
  const registry = {}, proxyCache = new WeakMap(), privateData = new WeakMap();
3
3
  const forPools = new WeakMap();
4
-
4
+ const activeComponents = new Set();
5
+
5
6
  const sfcLogic = new Map();
6
- const sharedStates = new Map(); // Track singleton states for $registry
7
+ const sharedStates = new Map();
8
+
9
+ const styleRegistry = new Map();
10
+ let styleConfig = {};
11
+
7
12
  const routes = [];
8
13
 
9
14
  const escapeHTML = (str) => {
@@ -13,26 +18,68 @@ const Lego = (() => {
13
18
  }[m]));
14
19
  };
15
20
 
21
+ /**
22
+ * Enterprise Target Resolver
23
+ * Resolves strings (#id, tag-name) or functions into DOM Elements.
24
+ * This is crucial for the $go router helper to know where to inject content
25
+ */
26
+ const resolveTargets = (query, contextEl) => {
27
+ if (typeof query === 'function') {
28
+ const all = Array.from(document.querySelectorAll('*')).filter(n => n.tagName.includes('-'));
29
+ return [].concat(query(all));
30
+ }
31
+ if (query.startsWith('#')) {
32
+ const el = document.getElementById(query.slice(1));
33
+ return el ? [el] : [];
34
+ }
35
+ // Scoped search first (within the calling component), then global fallback
36
+ const scoped = contextEl?.querySelectorAll(query) || [];
37
+ return scoped.length > 0 ? [...scoped] : [...document.querySelectorAll(query)];
38
+ };
39
+
40
+ /**
41
+ * Universal Routing Helper
42
+ * Shared between Lego.globals.$go and template helpers
43
+ */
44
+ const _go = (path, ...targets) => (contextEl) => {
45
+ const execute = async (method, body = null, pushState = true, options = {}) => {
46
+ if (pushState) {
47
+ const serializedTargets = targets.filter(t => typeof t === 'string');
48
+ const state = { legoTargets: serializedTargets, method, body };
49
+ history.pushState(state, '', path);
50
+ }
51
+ await _matchRoute(targets.length ? targets : null, contextEl);
52
+ };
53
+
54
+ return {
55
+ get: (push = true, opt = {}) => execute('GET', null, push, opt),
56
+ post: (data, push = true, opt = {}) => execute('POST', data, push, opt),
57
+ put: (data, push = true, opt = {}) => execute('PUT', data, push, opt),
58
+ patch: (data, push = true, opt = {}) => execute('PATCH', data, push, opt),
59
+ delete: (push = true, opt = {}) => execute('DELETE', null, push, opt)
60
+ };
61
+ };
62
+
16
63
  const createBatcher = () => {
17
64
  let queued = false;
18
65
  const componentsToUpdate = new Set();
19
66
  let isProcessing = false;
20
-
67
+
21
68
  return {
22
69
  add: (el) => {
23
- if (!el || isProcessing) return;
70
+ if (!el || isProcessing) return;
24
71
  componentsToUpdate.add(el);
25
72
  if (queued) return;
26
73
  queued = true;
27
-
74
+
28
75
  requestAnimationFrame(() => {
29
76
  isProcessing = true;
30
77
  const batch = Array.from(componentsToUpdate);
31
78
  componentsToUpdate.clear();
32
79
  queued = false;
33
-
80
+
34
81
  batch.forEach(el => render(el));
35
-
82
+
36
83
  setTimeout(() => {
37
84
  batch.forEach(el => {
38
85
  const state = el._studs;
@@ -94,7 +141,7 @@ const Lego = (() => {
94
141
 
95
142
  const getPrivateData = (el) => {
96
143
  if (!privateData.has(el)) {
97
- privateData.set(el, { snapped: false, bindings: null, bound: false, rendering: false });
144
+ privateData.set(el, { snapped: false, bindings: null, bound: false, rendering: false, anchor: null, hasGlobalDependency: false });
98
145
  }
99
146
  return privateData.get(el);
100
147
  };
@@ -124,12 +171,18 @@ const Lego = (() => {
124
171
  const safeEval = (expr, context) => {
125
172
  try {
126
173
  const scope = context.state || {};
127
-
174
+
128
175
  const helpers = {
129
176
  $ancestors: (tag) => findAncestorState(context.self, tag),
130
- // Helper to access shared state by tag name
131
177
  $registry: (tag) => sharedStates.get(tag.toLowerCase()),
132
178
  $element: context.self,
179
+ $route: Lego.globals.$route,
180
+ /**
181
+ * The $go helper for surgical routing
182
+ * @param {string} path - URL to navigate to
183
+ * @param {...(string|function)} targets - Specific components or IDs to swap
184
+ */
185
+ $go: (path, ...targets) => _go(path, ...targets)(context.self),
133
186
  $emit: (name, detail) => {
134
187
  context.self.dispatchEvent(new CustomEvent(name, {
135
188
  detail,
@@ -146,7 +199,7 @@ const Lego = (() => {
146
199
  }
147
200
  }
148
201
  `);
149
-
202
+
150
203
  const result = func.call(scope, context.global, context.self, context.event, helpers);
151
204
  if (typeof result === 'function') return result.call(scope, context.event);
152
205
  return result;
@@ -167,10 +220,10 @@ const Lego = (() => {
167
220
  const bind = (container, componentRoot, loopCtx = null) => {
168
221
  const state = componentRoot._studs;
169
222
  const elements = container instanceof Element ? [container, ...container.querySelectorAll('*')] : container.querySelectorAll('*');
170
-
223
+
171
224
  elements.forEach(child => {
172
225
  const childData = getPrivateData(child);
173
- if (childData.bound) return;
226
+ if (childData.bound) return;
174
227
 
175
228
  [...child.attributes].forEach(attr => {
176
229
  if (attr.name.startsWith('@')) {
@@ -178,7 +231,7 @@ const Lego = (() => {
178
231
  child.addEventListener(eventName, (event) => {
179
232
  let evalScope = state;
180
233
  if (loopCtx) {
181
- const list = resolve(loopCtx.listName, state);
234
+ const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
182
235
  const item = list[loopCtx.index];
183
236
  evalScope = Object.assign(Object.create(state), { [loopCtx.name]: item });
184
237
  }
@@ -192,7 +245,7 @@ const Lego = (() => {
192
245
  const updateState = () => {
193
246
  let target, last;
194
247
  if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
195
- const list = resolve(loopCtx.listName, state);
248
+ const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
196
249
  const item = list[loopCtx.index];
197
250
  if (!item) return;
198
251
  const subPath = prop.split('.').slice(1);
@@ -218,38 +271,65 @@ const Lego = (() => {
218
271
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
219
272
  let node;
220
273
  while (node = walker.nextNode()) {
221
- const isInsideBFor = (n) => {
274
+ const isInsideBoundary = (n) => {
222
275
  let curr = n.parentNode;
223
276
  while (curr && curr !== container) {
224
277
  if (curr.hasAttribute && curr.hasAttribute('b-for')) return true;
225
- if (curr.tagName && curr.tagName.includes('-') && registry[curr.tagName.toLowerCase()]) return true;
278
+ // Only stop at Shadow Roots or explicit boundaries, NOT component tags in Light DOM
279
+ // The parent MUST be able to bind data to the slots of its children.
226
280
  curr = curr.parentNode;
227
281
  }
228
282
  return false;
229
283
  };
230
- if (isInsideBFor(node)) continue;
284
+ if (isInsideBoundary(node)) continue;
285
+
286
+ const checkGlobal = (str) => {
287
+ if (/\bglobal\b/.test(str)) {
288
+ const target = container.host || container;
289
+ getPrivateData(target).hasGlobalDependency = true;
290
+ }
291
+ };
231
292
 
232
- if (node.nodeType === 1) {
233
- if (node.hasAttribute('b-if')) bindings.push({ type: 'b-if', node, expr: node.getAttribute('b-if') });
293
+ if (node.nodeType === Node.ELEMENT_NODE) {
294
+ if (node.hasAttribute('b-if')) {
295
+ const expr = node.getAttribute('b-if');
296
+ checkGlobal(expr);
297
+ // Create an anchor point to keep track of where the element belongs in the DOM
298
+ const anchor = document.createComment(`b-if: ${expr}`);
299
+ const data = getPrivateData(node);
300
+ data.anchor = anchor;
301
+ bindings.push({ type: 'b-if', node, anchor, expr });
302
+ }
303
+
304
+ if (node.hasAttribute('b-show')) {
305
+ const expr = node.getAttribute('b-show');
306
+ checkGlobal(expr);
307
+ bindings.push({ type: 'b-show', node, expr });
308
+ }
234
309
  if (node.hasAttribute('b-for')) {
235
310
  const match = node.getAttribute('b-for').match(/^\s*(\w+)\s+in\s+(.+)\s*$/);
236
311
  if (match) {
237
- bindings.push({
238
- type: 'b-for',
239
- node,
240
- itemName: match[1],
241
- listName: match[2].trim(),
242
- template: node.innerHTML
312
+ checkGlobal(match[2]);
313
+ bindings.push({
314
+ type: 'b-for',
315
+ node,
316
+ itemName: match[1],
317
+ listName: match[2].trim(),
318
+ template: node.innerHTML
243
319
  });
244
- node.innerHTML = '';
320
+ node.innerHTML = '';
245
321
  }
246
322
  }
247
323
  if (node.hasAttribute('b-text')) bindings.push({ type: 'b-text', node, path: node.getAttribute('b-text') });
248
324
  if (node.hasAttribute('b-sync')) bindings.push({ type: 'b-sync', node });
249
325
  [...node.attributes].forEach(attr => {
250
- if (attr.value.includes('{{')) bindings.push({ type: 'attr', node, attrName: attr.name, template: attr.value });
326
+ if (attr.value.includes('{{')) {
327
+ checkGlobal(attr.value);
328
+ bindings.push({ type: 'attr', node, attrName: attr.name, template: attr.value });
329
+ }
251
330
  });
252
- } else if (node.nodeType === 3 && node.textContent.includes('{{')) {
331
+ } else if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('{{')) {
332
+ checkGlobal(node.textContent);
253
333
  bindings.push({ type: 'text', node, template: node.textContent });
254
334
  }
255
335
  }
@@ -258,15 +338,15 @@ const Lego = (() => {
258
338
 
259
339
  const updateNodeBindings = (root, scope) => {
260
340
  const processNode = (node) => {
261
- if (node.nodeType === 3) {
341
+ if (node.nodeType === Node.TEXT_NODE) {
262
342
  if (node._tpl === undefined) node._tpl = node.textContent;
263
- const out = node._tpl.replace(/{{(.*?)}}/g, (_, k) => escapeHTML(safeEval(k.trim(), { state: scope, self: node }) ?? ''));
343
+ const out = node._tpl.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state: scope, global: Lego.globals, self: node }) ?? '');
264
344
  if (node.textContent !== out) node.textContent = out;
265
- } else if (node.nodeType === 1) {
345
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
266
346
  [...node.attributes].forEach(attr => {
267
347
  if (attr._tpl === undefined) attr._tpl = attr.value;
268
348
  if (attr._tpl.includes('{{')) {
269
- const out = attr._tpl.replace(/{{(.*?)}}/g, (_, k) => escapeHTML(safeEval(k.trim(), { state: scope, self: node }) ?? ''));
349
+ const out = attr._tpl.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state: scope, global: Lego.globals, self: node }) ?? '');
270
350
  if (attr.value !== out) {
271
351
  attr.value = out;
272
352
  if (attr.name === 'class') node.className = out;
@@ -289,27 +369,37 @@ const Lego = (() => {
289
369
  data.rendering = true;
290
370
 
291
371
  try {
292
- const shadow = el.shadowRoot;
293
- if (!shadow) return;
294
- if (!data.bindings) data.bindings = scanForBindings(shadow);
372
+ // Use shadowRoot if it's a component, otherwise render the element itself (light DOM)
373
+ const target = el.shadowRoot || el;
374
+ if (!data.bindings) data.bindings = scanForBindings(target);
295
375
 
296
376
  data.bindings.forEach(b => {
297
- if (b.type === 'b-if') b.node.style.display = safeEval(b.expr, { state, self: b.node }) ? '' : 'none';
298
- if (b.type === 'b-text') b.node.textContent = escapeHTML(resolve(b.path, state));
377
+ // ... (binding logic remains same, just uses b.node which is relative to target)
378
+ if (b.type === 'b-if') {
379
+ const condition = !!safeEval(b.expr, { state, global: Lego.globals, self: b.node });
380
+ const isAttached = !!b.node.parentNode;
381
+ if (condition && !isAttached) {
382
+ if (b.anchor.parentNode) b.anchor.parentNode.replaceChild(b.node, b.anchor);
383
+ } else if (!condition && isAttached) {
384
+ b.node.parentNode.replaceChild(b.anchor, b.node);
385
+ }
386
+ }
387
+ if (b.type === 'b-show') b.node.style.display = safeEval(b.expr, { state, global: Lego.globals, self: b.node }) ? '' : 'none';
388
+ if (b.type === 'b-text') b.node.textContent = resolve(b.path, state);
299
389
  if (b.type === 'b-sync') syncModelValue(b.node, resolve(b.node.getAttribute('b-sync'), state));
300
390
  if (b.type === 'text') {
301
- const out = b.template.replace(/{{(.*?)}}/g, (_, k) => escapeHTML(safeEval(k.trim(), { state, self: b.node }) ?? ''));
391
+ const out = b.template.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state, global: Lego.globals, self: b.node }) ?? '');
302
392
  if (b.node.textContent !== out) b.node.textContent = out;
303
393
  }
304
394
  if (b.type === 'attr') {
305
- const out = b.template.replace(/{{(.*?)}}/g, (_, k) => escapeHTML(safeEval(k.trim(), { state, self: b.node }) ?? ''));
395
+ const out = b.template.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state, global: Lego.globals, self: b.node }) ?? '');
306
396
  if (b.node.getAttribute(b.attrName) !== out) {
307
397
  b.node.setAttribute(b.attrName, out);
308
398
  if (b.attrName === 'class') b.node.className = out;
309
399
  }
310
400
  }
311
401
  if (b.type === 'b-for') {
312
- const list = resolve(b.listName, state) || [];
402
+ const list = safeEval(b.listName, { state, global: Lego.globals, self: el }) || [];
313
403
  if (!forPools.has(b.node)) forPools.set(b.node, new Map());
314
404
  const pool = forPools.get(b.node);
315
405
  const currentKeys = new Set();
@@ -326,12 +416,13 @@ const Lego = (() => {
326
416
  }
327
417
  const localScope = Object.assign(Object.create(state), { [b.itemName]: item });
328
418
  updateNodeBindings(child, localScope);
329
-
419
+
330
420
  child.querySelectorAll('[b-sync]').forEach(input => {
331
- const path = input.getAttribute('b-sync');
332
- if (path.startsWith(b.itemName + '.')) {
333
- syncModelValue(input, resolve(path.split('.').slice(1).join('.'), item));
334
- }
421
+ const path = input.getAttribute('b-sync');
422
+ if (path.startsWith(b.itemName + '.')) {
423
+ const list = safeEval(b.listName, { state, global: Lego.globals, self: el });
424
+ syncModelValue(input, resolve(path.split('.').slice(1).join('.'), list[i]));
425
+ }
335
426
  });
336
427
  if (b.node.children[i] !== child) b.node.insertBefore(child, b.node.children[i] || null);
337
428
  });
@@ -340,42 +431,71 @@ const Lego = (() => {
340
431
  }
341
432
  }
342
433
  });
434
+
435
+ // Global Broadcast: Only notify components that depend on globals
436
+ if (state === Lego.globals) {
437
+ activeComponents.forEach(comp => {
438
+ if (getPrivateData(comp).hasGlobalDependency) render(comp);
439
+ });
440
+ }
343
441
  } finally {
344
442
  data.rendering = false;
345
443
  }
346
444
  };
347
445
 
348
446
  const snap = (el) => {
349
- if (!el || el.nodeType !== 1) return;
447
+ if (!el || el.nodeType !== Node.ELEMENT_NODE) return;
350
448
  const data = getPrivateData(el);
351
449
  const name = el.tagName.toLowerCase();
352
-
353
- if (registry[name] && !data.snapped) {
450
+ const templateNode = registry[name];
451
+
452
+ if (templateNode && !data.snapped) {
354
453
  data.snapped = true;
355
- const tpl = registry[name].content.cloneNode(true);
454
+ const tpl = templateNode.content.cloneNode(true);
356
455
  const shadow = el.attachShadow({ mode: 'open' });
357
-
358
- const defaultLogic = sfcLogic.get(name) || {};
359
- const attrLogic = parseJSObject(el.getAttribute('b-data') || '{}');
360
- el._studs = reactive({ ...defaultLogic, ...attrLogic }, el);
361
-
456
+
457
+ const styleKeys = (templateNode.getAttribute('b-styles') || "").split(/\s+/).filter(Boolean);
458
+ if (styleKeys.length > 0) {
459
+ const sheetsToApply = styleKeys.flatMap(key => styleRegistry.get(key) || []);
460
+ if (sheetsToApply.length > 0) {
461
+ shadow.adoptedStyleSheets = [...sheetsToApply];
462
+ }
463
+ }
464
+
465
+ // TIER 1: Logic from Lego.define (SFC)
466
+ // TIER 2: Logic from the <template b-data="..."> attribute
467
+ // TIER 3: Logic from the <my-comp b-data="..."> tag
468
+ const scriptLogic = sfcLogic.get(name) || {};
469
+ const templateLogic = parseJSObject(templateNode.getAttribute('b-data') || '{}');
470
+ const instanceLogic = parseJSObject(el.getAttribute('b-data') || '{}');
471
+
472
+ // Priority: Script < Template < Instance
473
+ el._studs = reactive({
474
+ ...scriptLogic,
475
+ ...templateLogic,
476
+ ...instanceLogic,
477
+ get $route() { return Lego.globals.$route },
478
+ get $go() { return Lego.globals.$go }
479
+ }, el);
480
+
362
481
  shadow.appendChild(tpl);
363
-
482
+
364
483
  const style = shadow.querySelector('style');
365
484
  if (style) {
366
485
  style.textContent = style.textContent.replace(/\bself\b/g, ':host');
367
486
  }
368
-
487
+
369
488
  bind(shadow, el);
489
+ activeComponents.add(el);
370
490
  render(el);
371
491
 
372
492
  if (typeof el._studs.mounted === 'function') {
373
493
  try { el._studs.mounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in mounted <${name}>:`, e); }
374
494
  }
375
495
  }
376
-
496
+
377
497
  let provider = el.parentElement;
378
- while(provider && !provider._studs) provider = provider.parentElement;
498
+ while (provider && !provider._studs) provider = provider.parentElement;
379
499
  if (provider && provider._studs) bind(el, provider);
380
500
 
381
501
  [...el.children].forEach(snap);
@@ -385,61 +505,139 @@ const Lego = (() => {
385
505
  if (el._studs && typeof el._studs.unmounted === 'function') {
386
506
  try { el._studs.unmounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in unmounted:`, e); }
387
507
  }
508
+ activeComponents.delete(el);
388
509
  [...el.children].forEach(unsnap);
389
510
  };
390
511
 
391
- const _matchRoute = async () => {
512
+ const _matchRoute = async (targetQueries = null, contextEl = null) => {
392
513
  const path = window.location.pathname;
514
+ const search = window.location.search;
393
515
  const match = routes.find(r => r.regex.test(path));
394
- const outlet = document.querySelector('lego-router');
395
- if (!outlet || !match) return;
516
+ if (!match) return;
517
+
518
+ // Resolve targets: Functional selectors > List of queries > History State > Default lego-router
519
+ let resolvedElements = [];
520
+ if (targetQueries) {
521
+ resolvedElements = targetQueries.flatMap(query => resolveTargets(query, contextEl));
522
+ } else {
523
+ const defaultOutlet = document.querySelector('lego-router');
524
+ if (defaultOutlet) resolvedElements = [defaultOutlet];
525
+ }
526
+
527
+ if (resolvedElements.length === 0) return;
396
528
 
397
529
  const values = path.match(match.regex).slice(1);
398
530
  const params = Object.fromEntries(match.paramNames.map((n, i) => [n, values[i]]));
531
+ const query = Object.fromEntries(new URLSearchParams(search));
399
532
 
400
533
  if (match.middleware) {
401
534
  const allowed = await match.middleware(params, Lego.globals);
402
- if (!allowed) return;
535
+ if (!allowed) return;
403
536
  }
404
537
 
405
- Lego.globals.params = params;
406
- outlet.innerHTML = `<${match.tagName}></${match.tagName}>`;
538
+ Lego.globals.$route.url = path + search;
539
+ Lego.globals.$route.route = match.path;
540
+ Lego.globals.$route.params = params;
541
+ Lego.globals.$route.query = query;
542
+ Lego.globals.$route.method = history.state?.method || 'GET';
543
+ Lego.globals.$route.body = history.state?.body || null;
544
+
545
+ resolvedElements.forEach(el => {
546
+ if (el) {
547
+ const component = document.createElement(match.tagName);
548
+ // Atomic swap: MutationObserver in init() will pick this up
549
+ el.replaceChildren(component);
550
+ }
551
+ });
407
552
  };
408
553
 
409
554
  return {
410
- init: () => {
411
- document.querySelectorAll('template[b-id]').forEach(t => registry[t.getAttribute('b-id')] = t);
412
- const observer = new MutationObserver(m => m.forEach(r => {
413
- r.addedNodes.forEach(n => n.nodeType === 1 && snap(n));
414
- r.removedNodes.forEach(n => n.nodeType === 1 && unsnap(n));
555
+ init: async (root = document.body, styles = {}) => {
556
+ // If called as an event listener or with invalid root, fail over to document.body
557
+ if (!root || typeof root.nodeType !== 'number') root = document.body;
558
+ styleConfig = styles;
559
+
560
+ // Pre-load all defined style sets into Constructable Stylesheets
561
+ const loadPromises = Object.entries(styles).map(async ([key, urls]) => {
562
+ const sheets = await Promise.all(urls.map(async (url) => {
563
+ try {
564
+ const response = await fetch(url);
565
+ const cssText = await response.text();
566
+ const sheet = new CSSStyleSheet();
567
+ await sheet.replace(cssText);
568
+ return sheet;
569
+ } catch (e) {
570
+ console.error(`[Lego] Failed to load stylesheet: ${url}`, e);
571
+ return null;
572
+ }
573
+ }));
574
+ styleRegistry.set(key, sheets.filter(s => s !== null));
575
+ });
576
+ await Promise.all(loadPromises);
577
+
578
+ document.querySelectorAll('template[b-id]').forEach(t => {
579
+ registry[t.getAttribute('b-id')] = t;
580
+ });
581
+
582
+ const observer = new MutationObserver(m => m.forEach(r => {
583
+ r.addedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && snap(n));
584
+ r.removedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && unsnap(n));
415
585
  }));
416
- observer.observe(document.body, { childList: true, subtree: true });
417
- snap(document.body);
586
+ observer.observe(root, { childList: true, subtree: true });
587
+
588
+ root._studs = Lego.globals;
589
+ snap(root);
590
+ bind(root, root);
591
+ render(root);
418
592
 
419
593
  if (routes.length > 0) {
420
- window.addEventListener('popstate', _matchRoute);
594
+ // Smart History: Restore surgical targets on Back button
595
+ window.addEventListener('popstate', (event) => {
596
+ const targets = event.state?.legoTargets || null;
597
+ _matchRoute(targets);
598
+ });
599
+
600
+ document.addEventListener('submit', e => {
601
+ e.preventDefault();
602
+ })
603
+
421
604
  document.addEventListener('click', e => {
422
- const link = e.target.closest('a[b-link]');
605
+ const path = e.composedPath();
606
+ const link = path.find(el => el.tagName === 'A' && (el.hasAttribute('b-target') || el.hasAttribute('b-link')));
423
607
  if (link) {
424
608
  e.preventDefault();
425
- history.pushState({}, '', link.getAttribute('href'));
426
- _matchRoute();
609
+ const href = link.getAttribute('href');
610
+ const targetAttr = link.getAttribute('b-target');
611
+ const targets = targetAttr ? targetAttr.split(/\s+/).filter(Boolean) : [];
612
+
613
+ const shouldPush = link.getAttribute('b-link') !== 'false';
614
+ Lego.globals.$go(href, ...targets).get(shouldPush);
427
615
  }
428
616
  });
429
617
  _matchRoute();
430
618
  }
431
619
  },
432
- globals: reactive({}, document.body),
433
- define: (tagName, templateHTML, logic = {}) => {
620
+ globals: reactive({
621
+ $route: {
622
+ url: window.location.pathname,
623
+ route: '',
624
+ params: {},
625
+ query: {},
626
+ method: 'GET',
627
+ body: null
628
+ },
629
+ $go: (path, ...targets) => _go(path, ...targets)(document.body)
630
+ }, document.body),
631
+ define: (tagName, templateHTML, logic = {}, styles = "") => {
434
632
  const t = document.createElement('template');
435
633
  t.setAttribute('b-id', tagName);
634
+ t.setAttribute('b-styles', styles);
436
635
  t.innerHTML = templateHTML;
437
636
  registry[tagName] = t;
438
637
  sfcLogic.set(tagName, logic);
439
-
440
- // Initialize shared state for $registry singleton
638
+
441
639
  sharedStates.set(tagName.toLowerCase(), reactive({ ...logic }, document.body));
442
-
640
+
443
641
  document.querySelectorAll(tagName).forEach(snap);
444
642
  },
445
643
  route: (path, tagName, middleware = null) => {
@@ -448,12 +646,11 @@ const Lego = (() => {
448
646
  paramNames.push(name);
449
647
  return '([^/]+)';
450
648
  });
451
- routes.push({ regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
649
+ routes.push({ path, regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
452
650
  }
453
651
  };
454
652
  })();
455
653
 
456
654
  if (typeof window !== 'undefined') {
457
- document.addEventListener('DOMContentLoaded', Lego.init);
458
655
  window.Lego = Lego;
459
656
  }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "lego-dom",
3
- "version": "0.0.8",
3
+ "version": "1.0.0",
4
4
  "license": "MIT",
5
5
  "description": "A feature-rich web components + SFC frontend framework",
6
6
  "main": "main.js",
7
7
  "type": "module",
8
8
  "exports": {
9
- ".": "./main.js",
9
+ ".": "./lego.js",
10
10
  "./main.js": "./main.js",
11
+ "./lego.js": "./lego.js",
11
12
  "./vite-plugin": "./vite-plugin.js",
12
13
  "./parse-lego": "./parse-lego.js"
13
14
  },
@@ -18,7 +19,7 @@
18
19
  "lego",
19
20
  "legokit"
20
21
  ],
21
- "author": "",
22
+ "author": "Tersoo Ortserga",
22
23
  "scripts": {
23
24
  "test": "vitest run",
24
25
  "docs:dev": "vitepress dev docs",
@@ -40,5 +41,9 @@
40
41
  },
41
42
  "dependencies": {
42
43
  "fast-glob": "^3.3.2"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/rayattack/legodom.git"
43
48
  }
44
49
  }