lego-dom 0.0.9 → 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 (166) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -0
  3. package/{go.html → cdn.html} +33 -26
  4. package/docs/.vitepress/config.js +39 -1
  5. package/docs/api/directives.md +3 -3
  6. package/docs/api/index.md +1 -1
  7. package/docs/contributing/01-welcome.md +36 -0
  8. package/docs/contributing/02-registry.md +99 -0
  9. package/docs/contributing/03-batcher.md +110 -0
  10. package/docs/contributing/04-reactivity.md +87 -0
  11. package/docs/contributing/05-caching.md +59 -0
  12. package/docs/contributing/06-init.md +125 -0
  13. package/docs/contributing/07-observer.md +69 -0
  14. package/docs/contributing/08-snap.md +126 -0
  15. package/docs/contributing/09-diffing.md +69 -0
  16. package/docs/contributing/10-studs.md +76 -0
  17. package/docs/contributing/11-scanner.md +104 -0
  18. package/docs/contributing/12-render.md +116 -0
  19. package/docs/contributing/13-directives.md +225 -0
  20. package/docs/contributing/14-events.md +57 -0
  21. package/docs/contributing/15-router.md +9 -0
  22. package/docs/contributing/16-state.md +48 -0
  23. package/docs/contributing/17-legodom.md +55 -0
  24. package/docs/contributing/index.md +5 -0
  25. package/docs/examples/form.md +1 -1
  26. package/docs/examples/index.md +1 -1
  27. package/docs/examples/routing.md +4 -4
  28. package/docs/examples/todo-app.md +1 -1
  29. package/docs/guide/cdn-usage.md +8 -0
  30. package/docs/guide/components.md +33 -15
  31. package/docs/guide/directives.md +22 -22
  32. package/docs/guide/getting-started.md +35 -10
  33. package/docs/guide/index.md +3 -3
  34. package/docs/guide/quick-start.md +4 -1
  35. package/docs/guide/reactivity.md +22 -1
  36. package/docs/guide/routing.md +189 -289
  37. package/docs/guide/sfc.md +1 -1
  38. package/docs/guide/templating.md +2 -2
  39. package/docs/index.md +41 -7
  40. package/docs/router/basic-routing.md +103 -0
  41. package/docs/router/cold-entry.md +91 -0
  42. package/docs/router/history.md +69 -0
  43. package/docs/router/index.md +73 -0
  44. package/docs/router/resolver.md +74 -0
  45. package/docs/router/surgical-swaps.md +134 -0
  46. package/examples/vite-app/index.html +4 -12
  47. package/examples/vite-app/package.json +4 -2
  48. package/examples/vite-app/src/app.css +3 -0
  49. package/examples/vite-app/src/app.js +29 -0
  50. package/examples/vite-app/src/components/app-navbar.lego +34 -0
  51. package/examples/vite-app/src/components/customers/customer-details.lego +24 -0
  52. package/examples/vite-app/src/components/customers/customer-orders.lego +21 -0
  53. package/examples/vite-app/src/components/customers/order-list.lego +55 -0
  54. package/examples/vite-app/src/components/greeting-card.lego +1 -1
  55. package/examples/vite-app/src/components/sample-component.lego +15 -15
  56. package/examples/vite-app/src/components/shells/customers-shell.lego +21 -0
  57. package/examples/vite-app/src/components/todo-list.lego +12 -15
  58. package/examples/vite-app/src/components/widgets/user-card.lego +27 -0
  59. package/examples/vite-app/vite.config.js +5 -1
  60. package/main.js +247 -56
  61. package/package.json +1 -1
  62. package/parse-lego.js +17 -8
  63. package/{main.test.js → tests/main.test.js} +34 -17
  64. package/tests/parse-lego.test.js +65 -0
  65. package/vite-plugin.js +60 -22
  66. package/docs/.vitepress/dist/404.html +0 -22
  67. package/docs/.vitepress/dist/api/define.html +0 -35
  68. package/docs/.vitepress/dist/api/directives.html +0 -32
  69. package/docs/.vitepress/dist/api/globals.html +0 -27
  70. package/docs/.vitepress/dist/api/index.html +0 -25
  71. package/docs/.vitepress/dist/api/lifecycle.html +0 -38
  72. package/docs/.vitepress/dist/api/route.html +0 -34
  73. package/docs/.vitepress/dist/api/vite-plugin.html +0 -37
  74. package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.js +0 -11
  75. package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.lean.js +0 -1
  76. package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.js +0 -8
  77. package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.lean.js +0 -1
  78. package/docs/.vitepress/dist/assets/api_globals.md.CEznyRAY.js +0 -3
  79. package/docs/.vitepress/dist/assets/api_globals.md.CEznyRAY.lean.js +0 -1
  80. package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.js +0 -1
  81. package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.lean.js +0 -1
  82. package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.js +0 -14
  83. package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.lean.js +0 -1
  84. package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.js +0 -10
  85. package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.lean.js +0 -1
  86. package/docs/.vitepress/dist/assets/api_vite-plugin.md.DC8Li09k.js +0 -13
  87. package/docs/.vitepress/dist/assets/api_vite-plugin.md.DC8Li09k.lean.js +0 -1
  88. package/docs/.vitepress/dist/assets/app.BfblNDJy.js +0 -1
  89. package/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.Crdp7-Zp.js +0 -1
  90. package/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.C18E44rY.js +0 -9
  91. package/docs/.vitepress/dist/assets/chunks/framework.B7OFBR9X.js +0 -19
  92. package/docs/.vitepress/dist/assets/chunks/theme.VX3itTW6.js +0 -2
  93. package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.js +0 -34
  94. package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.lean.js +0 -1
  95. package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.js +0 -28
  96. package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.lean.js +0 -1
  97. package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.js +0 -338
  98. package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.lean.js +0 -1
  99. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.js +0 -13
  100. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.lean.js +0 -1
  101. package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.js +0 -297
  102. package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.lean.js +0 -1
  103. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.js +0 -182
  104. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.lean.js +0 -1
  105. package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.js +0 -174
  106. package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.lean.js +0 -1
  107. package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.js +0 -1
  108. package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.lean.js +0 -1
  109. package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.js +0 -140
  110. package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.lean.js +0 -1
  111. package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.js +0 -107
  112. package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.lean.js +0 -1
  113. package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.js +0 -2
  114. package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.lean.js +0 -1
  115. package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.js +0 -304
  116. package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.lean.js +0 -1
  117. package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.js +0 -33
  118. package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.lean.js +0 -1
  119. package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.js +0 -135
  120. package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.lean.js +0 -1
  121. package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.js +0 -193
  122. package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.lean.js +0 -1
  123. package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.js +0 -187
  124. package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.lean.js +0 -1
  125. package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.js +0 -119
  126. package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.lean.js +0 -1
  127. package/docs/.vitepress/dist/assets/index.md.M4_o26kF.js +0 -23
  128. package/docs/.vitepress/dist/assets/index.md.M4_o26kF.lean.js +0 -1
  129. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  130. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  131. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  132. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  133. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  134. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  135. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  136. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  137. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  138. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  139. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  140. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  141. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  142. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  143. package/docs/.vitepress/dist/assets/style.eycE2Jhw.css +0 -1
  144. package/docs/.vitepress/dist/examples/form.html +0 -58
  145. package/docs/.vitepress/dist/examples/index.html +0 -52
  146. package/docs/.vitepress/dist/examples/routing.html +0 -362
  147. package/docs/.vitepress/dist/examples/sfc-showcase.html +0 -37
  148. package/docs/.vitepress/dist/examples/todo-app.html +0 -321
  149. package/docs/.vitepress/dist/guide/cdn-usage.html +0 -206
  150. package/docs/.vitepress/dist/guide/components.html +0 -198
  151. package/docs/.vitepress/dist/guide/contributing.html +0 -25
  152. package/docs/.vitepress/dist/guide/directives.html +0 -164
  153. package/docs/.vitepress/dist/guide/getting-started.html +0 -131
  154. package/docs/.vitepress/dist/guide/index.html +0 -26
  155. package/docs/.vitepress/dist/guide/lifecycle.html +0 -328
  156. package/docs/.vitepress/dist/guide/quick-start.html +0 -57
  157. package/docs/.vitepress/dist/guide/reactivity.html +0 -159
  158. package/docs/.vitepress/dist/guide/routing.html +0 -217
  159. package/docs/.vitepress/dist/guide/sfc.html +0 -211
  160. package/docs/.vitepress/dist/guide/templating.html +0 -143
  161. package/docs/.vitepress/dist/hashmap.json +0 -1
  162. package/docs/.vitepress/dist/index.html +0 -47
  163. package/docs/.vitepress/dist/logo.svg +0 -38
  164. package/docs/.vitepress/dist/vp-icons.css +0 -1
  165. package/examples/vite-app/src/main.js +0 -11
  166. 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
+ const activeComponents = new Set();
4
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,6 +18,48 @@ 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();
@@ -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
  };
@@ -127,9 +174,15 @@ const Lego = (() => {
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,
@@ -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,22 +271,45 @@ 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
+ };
292
+
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
+ }
231
303
 
232
- if (node.nodeType === 1) {
233
- if (node.hasAttribute('b-if')) bindings.push({ type: 'b-if', node, expr: node.getAttribute('b-if') });
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) {
312
+ checkGlobal(match[2]);
237
313
  bindings.push({
238
314
  type: 'b-for',
239
315
  node,
@@ -247,9 +323,13 @@ const Lego = (() => {
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();
@@ -330,7 +420,8 @@ const Lego = (() => {
330
420
  child.querySelectorAll('[b-sync]').forEach(input => {
331
421
  const path = input.getAttribute('b-sync');
332
422
  if (path.startsWith(b.itemName + '.')) {
333
- syncModelValue(input, resolve(path.split('.').slice(1).join('.'), item));
423
+ const list = safeEval(b.listName, { state, global: Lego.globals, self: el });
424
+ syncModelValue(input, resolve(path.split('.').slice(1).join('.'), list[i]));
334
425
  }
335
426
  });
336
427
  if (b.node.children[i] !== child) b.node.insertBefore(child, b.node.children[i] || null);
@@ -340,24 +431,52 @@ 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();
450
+ const templateNode = registry[name];
352
451
 
353
- if (registry[name] && !data.snapped) {
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
456
 
358
- const defaultLogic = sfcLogic.get(name) || {};
359
- const attrLogic = parseJSObject(el.getAttribute('b-data') || '{}');
360
- el._studs = reactive({ ...defaultLogic, ...attrLogic }, el);
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);
361
480
 
362
481
  shadow.appendChild(tpl);
363
482
 
@@ -367,6 +486,7 @@ const Lego = (() => {
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') {
@@ -385,65 +505,137 @@ 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
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);
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
+
412
582
  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));
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
-
418
- // Also snap the root element (body) to catch attributes like @event
419
- snap(document.body);
586
+ observer.observe(root, { childList: true, subtree: true });
420
587
 
421
- // Bind body specifically to catch global listeners like @todo-added in go.html
422
- // We pass a mock componentRoot that points to Lego.globals
423
- bind(document.body, { _studs: Lego.globals, _data: { bound: false } });
588
+ root._studs = Lego.globals;
589
+ snap(root);
590
+ bind(root, root);
591
+ render(root);
424
592
 
425
593
  if (routes.length > 0) {
426
- 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
+
427
604
  document.addEventListener('click', e => {
428
- 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')));
429
607
  if (link) {
430
608
  e.preventDefault();
431
- history.pushState({}, '', link.getAttribute('href'));
432
- _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);
433
615
  }
434
616
  });
435
617
  _matchRoute();
436
618
  }
437
619
  },
438
- globals: reactive({}, document.body),
439
- 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 = "") => {
440
632
  const t = document.createElement('template');
441
633
  t.setAttribute('b-id', tagName);
634
+ t.setAttribute('b-styles', styles);
442
635
  t.innerHTML = templateHTML;
443
636
  registry[tagName] = t;
444
637
  sfcLogic.set(tagName, logic);
445
638
 
446
- // Initialize shared state for $registry singleton
447
639
  sharedStates.set(tagName.toLowerCase(), reactive({ ...logic }, document.body));
448
640
 
449
641
  document.querySelectorAll(tagName).forEach(snap);
@@ -454,12 +646,11 @@ const Lego = (() => {
454
646
  paramNames.push(name);
455
647
  return '([^/]+)';
456
648
  });
457
- routes.push({ regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
649
+ routes.push({ path, regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
458
650
  }
459
651
  };
460
652
  })();
461
653
 
462
654
  if (typeof window !== 'undefined') {
463
- document.addEventListener('DOMContentLoaded', Lego.init);
464
655
  window.Lego = Lego;
465
656
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lego-dom",
3
- "version": "0.0.9",
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",
package/parse-lego.js CHANGED
@@ -7,20 +7,28 @@
7
7
  * Parse a .lego file content into structured sections
8
8
  * @param {string} content - Raw .lego file content
9
9
  * @param {string} filename - Filename for error reporting
10
- * @returns {{template: string, script: string, style: string, componentName: string}}
10
+ * @returns {{template: string, script: string, style: string, stylesAttr: string, componentName: string}}
11
11
  */
12
12
  export function parseLego(content, filename = 'component.lego') {
13
13
  const result = {
14
14
  template: '',
15
15
  script: '',
16
16
  style: '',
17
+ stylesAttr: '', // Stores the value of the b-styles attribute
17
18
  componentName: deriveComponentName(filename)
18
19
  };
19
20
 
20
- // Extract template section
21
- const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
21
+ // Updated to capture attributes on the template tag (like b-styles)
22
+ const templateMatch = content.match(/<template([\s\S]*?)>([\s\S]*?)<\/template>/);
22
23
  if (templateMatch) {
23
- result.template = templateMatch[1].trim();
24
+ const attrs = templateMatch[1];
25
+ result.template = templateMatch[2].trim();
26
+
27
+ // Extract b-styles value from the attributes string
28
+ const bStylesMatch = attrs.match(/b-styles=["']([^"']+)["']/);
29
+ if (bStylesMatch) {
30
+ result.stylesAttr = bStylesMatch[1];
31
+ }
24
32
  }
25
33
 
26
34
  // Extract script section
@@ -51,11 +59,12 @@ export function deriveComponentName(filename) {
51
59
 
52
60
  /**
53
61
  * Generate Lego.define() code from parsed .lego file
62
+ * Updated to include the 4th argument for styles
54
63
  * @param {object} parsed - Parsed .lego file object
55
64
  * @returns {string} - JavaScript code string
56
65
  */
57
66
  export function generateDefineCall(parsed) {
58
- const { componentName, template, script, style } = parsed;
67
+ const { componentName, template, script, style, stylesAttr } = parsed;
59
68
 
60
69
  // Build template HTML
61
70
  let templateHTML = '';
@@ -79,8 +88,8 @@ export function generateDefineCall(parsed) {
79
88
  }
80
89
  }
81
90
 
82
- // Generate the Lego.define call
83
- return `Lego.define('${componentName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode});`;
91
+ // Generate the Lego.define call with the new 4th argument (stylesAttr)
92
+ return `Lego.define('${componentName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode}, '${stylesAttr}');`;
84
93
  }
85
94
 
86
95
  /**
@@ -116,4 +125,4 @@ export function validateLego(parsed) {
116
125
  valid: errors.length === 0,
117
126
  errors
118
127
  };
119
- }
128
+ }
@@ -11,23 +11,41 @@ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
11
11
 
12
12
  global.window = dom.window;
13
13
  global.document = dom.window.document;
14
- global.navigator = dom.window.navigator;
14
+
15
+ // Use defineProperty for navigator as it might be read-only in some environments
16
+ Object.defineProperty(global, 'navigator', {
17
+ value: dom.window.navigator,
18
+ writable: true,
19
+ configurable: true
20
+ });
15
21
  global.HTMLElement = dom.window.HTMLElement;
16
22
  global.customElements = dom.window.customElements;
17
23
  global.MutationObserver = dom.window.MutationObserver;
18
24
  global.Node = dom.window.Node;
19
25
  global.NodeFilter = dom.window.NodeFilter;
26
+ global.Element = dom.window.Element;
27
+ global.Event = dom.window.Event;
28
+ global.FormData = dom.window.FormData;
29
+
30
+ // Mock CSSStyleSheet for JSDOM
31
+ global.CSSStyleSheet = class {
32
+ constructor() { this.cssRules = []; }
33
+ replace(content) { this.cssText = content; return Promise.resolve(this); }
34
+ };
35
+
20
36
  global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
21
37
 
22
38
  // 2. Load the library code
23
39
  // We read it as a string to execute it in our shimmed global environment
24
- const libCode = fs.readFileSync(path.resolve(__dirname, './main.js'), 'utf8');
25
- eval(libCode);
40
+ const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
41
+ eval(libCode);
26
42
 
27
43
  describe('Lego JS Node Environment Tests', () => {
28
- beforeEach(() => {
44
+ beforeEach(async () => {
29
45
  document.body.innerHTML = '';
30
- // Reset registry if necessary between tests
46
+ window.clicked = false;
47
+ // Initializing the library manually for the test environment
48
+ await window.Lego.init(document.body);
31
49
  });
32
50
 
33
51
  it('should initialize the Lego global object', () => {
@@ -38,45 +56,44 @@ describe('Lego JS Node Environment Tests', () => {
38
56
  it('should reactively update text content', async () => {
39
57
  window.Lego.define('test-comp', '<span>{{msg}}</span>');
40
58
  const el = document.createElement('test-comp');
41
- el.setAttribute('l-studs', "{ msg: 'hello' }");
59
+ el.setAttribute('b-data', "{ msg: 'hello' }");
42
60
  document.body.appendChild(el);
43
61
 
44
- // Wait for Lego.init / MutationObserver to fire
45
- await new Promise(r => setTimeout(r, 50));
62
+ // Wait for MutationObserver / snap to fire
63
+ await new Promise(r => setTimeout(r, 100));
46
64
 
47
65
  const span = el.shadowRoot.querySelector('span');
48
66
  expect(span.textContent).toBe('hello');
49
67
 
50
68
  // Test reactivity
51
69
  el._studs.msg = 'world';
52
-
70
+
53
71
  // Wait for batcher (requestAnimationFrame shim)
54
- await new Promise(r => setTimeout(r, 50));
72
+ await new Promise(r => setTimeout(r, 100));
55
73
  expect(span.textContent).toBe('world');
56
74
  });
57
75
 
58
76
  it('should prevent XSS via auto-escaping', async () => {
59
77
  window.Lego.define('xss-comp', '<div>{{code}}</div>');
60
78
  const el = document.createElement('xss-comp');
61
- el.setAttribute('l-studs', "{ code: '<script>alert(1)</script>' }");
79
+ el.setAttribute('b-data', "{ code: '<script>alert(1)</script>' }");
62
80
  document.body.appendChild(el);
63
81
 
64
- await new Promise(r => setTimeout(r, 50));
65
-
82
+ await new Promise(r => setTimeout(r, 100));
83
+
66
84
  const div = el.shadowRoot.querySelector('div');
67
85
  // It should be escaped, not raw HTML
68
86
  expect(div.innerHTML).toContain('&lt;script&gt;');
69
87
  });
70
88
 
71
89
  it('should handle @events using the universal binder', async () => {
72
- const clickSpy = vi.fn();
73
90
  window.Lego.define('event-comp', '<button @click="handleClick">Click Me</button>');
74
-
91
+
75
92
  const el = document.createElement('event-comp');
76
- el.setAttribute('l-studs', `{ handleClick: () => { window.clicked = true; } }`);
93
+ el.setAttribute('b-data', `{ handleClick: () => { window.clicked = true; } }`);
77
94
  document.body.appendChild(el);
78
95
 
79
- await new Promise(r => setTimeout(r, 50));
96
+ await new Promise(r => setTimeout(r, 100));
80
97
 
81
98
  const btn = el.shadowRoot.querySelector('button');
82
99
  btn.click();