lego-dom 1.5.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ # Changelog
4
+
5
+ ## [2.0.2] - 2026-01-19
6
+
7
+ ### Features
8
+
9
+ - **Scoped Props (`b-logic`):** `b-logic` attributes now inherit the parent block's scope! You can pass state naturally to children without global variables.
10
+ ```html
11
+ <!-- 'user' is resolved from the parent block's state -->
12
+ <user-avatar b-logic="{ user: user }"></user-avatar>
13
+ ```
14
+
15
+ - **`$parent` Helper:** Added `this.$parent` (and template access) to easily find the nearest ancestor Block. It intelligently skips non-block elements (divs, spans) and handles Shadow DOM boundaries.
16
+ ```javascript
17
+ const parentBlock = this.$parent;
18
+ ```
19
+
20
+ ## [2.0.1] - 2026-01-19
21
+
22
+ ### The "Blocks" Update Refactor 🧱
23
+
24
+ Welcome to **LegoDOM v2**! This release represents a massive architectural shift to align the codebase and mental model with the "Lego" metaphor. We have moved away from Vue-centric terminology ("SFC", "Component", "Data") to Lego-centric terminology ("Lego File", "Block", "Logic").
25
+
26
+ ### Breaking Changes 🚨
27
+
28
+ - **Terminological Refactor (Runtime & Docs):**
29
+ - **Components → Blocks:** The fundamental unit of UI is now a "Block".
30
+ - **SFC → Lego File:** Protocol-agnostic Single File Blocks.
31
+
32
+ - **API Renames:**
33
+ - `Lego.define()` is now **`Lego.block()`**. (Legacy alias maintained)
34
+ - `Lego.defineSFC()` is now **`Lego.defineLegoFile()`**.
35
+ - Internal: `deriveComponentName` is now `deriveBlockName`.
36
+
37
+ - **Attribute Renames:**
38
+ - `b-data` is now **`b-logic`**. (Legacy alias maintained)
39
+ - `b-styles` is now **`b-stylesheets`**.
40
+
41
+ ### Features
42
+
43
+ - **Public State API:**
44
+ - `element.state` is now the official public API for accessing the reactive proxy.
45
+ - `element._studs` is considered internal/private.
46
+
47
+ - **Lego Studio Audit:**
48
+ - `lego-studio` has been refactored to use "Block" terminology throughout the UI and codebase.
49
+
3
50
  ## [1.5.1] - 2026-01-19
4
51
 
5
52
  ### Breaking Changes 🚨
package/main.js CHANGED
@@ -1,9 +1,9 @@
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
+ const activeBlocks = new Set();
5
5
 
6
- const sfcLogic = new Map();
6
+ const legoFileLogic = new Map();
7
7
  const sharedStates = new Map();
8
8
  const expressionCache = new Map(); // Cache for compiled expressions
9
9
 
@@ -39,7 +39,7 @@ const Lego = (() => {
39
39
  }[m]));
40
40
  };
41
41
 
42
- const deriveComponentName = (filename) => {
42
+ const deriveBlockName = (filename) => {
43
43
  const basename = filename.split('/').pop().replace(/\.lego$/, '');
44
44
  // 1. Convert snake_case to kebab-case
45
45
  // 2. Convert PascalCase/camelCase to kebab-case
@@ -49,7 +49,7 @@ const Lego = (() => {
49
49
  .toLowerCase();
50
50
 
51
51
  if (!name.includes('-')) {
52
- throw new Error(`[Lego] Invalid component definition: "${filename}". Component names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`);
52
+ throw new Error(`[Lego] Invalid block definition: "${filename}". Block names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`);
53
53
  }
54
54
  return name;
55
55
  };
@@ -68,7 +68,7 @@ const Lego = (() => {
68
68
  const el = document.getElementById(query.slice(1));
69
69
  return el ? [el] : [];
70
70
  }
71
- // Scoped search first (within the calling component), then global fallback
71
+ // Scoped search first (within the calling block), then global fallback
72
72
  const scoped = contextEl?.querySelectorAll(query) || [];
73
73
  return scoped.length > 0 ? [...scoped] : [...document.querySelectorAll(query)];
74
74
  };
@@ -176,11 +176,10 @@ const Lego = (() => {
176
176
  return p;
177
177
  };
178
178
 
179
- const parseJSObject = (raw) => {
179
+ const parseJSObject = (raw, scope = {}) => {
180
180
  try {
181
- return (new Function(`return (${raw})`))();
181
+ return (new Function('scope', 'global', `with(global) { with(scope) { return (${raw}); } }`))(scope, Lego.globals);
182
182
  } catch (e) {
183
- console.error(`[Lego] Failed to parse b-data:`, raw, e);
184
183
  return {};
185
184
  }
186
185
  };
@@ -204,10 +203,13 @@ const Lego = (() => {
204
203
  };
205
204
 
206
205
  const findAncestor = (el, tagName) => {
207
- let parent = el.parentElement || el.getRootNode().host;
206
+ if (!el) return undefined;
207
+ let parent = el.parentElement || (el.getRootNode ? el.getRootNode().host : null);
208
208
  while (parent) {
209
- if (parent.tagName && parent.tagName.toLowerCase() === tagName.toLowerCase()) {
210
- return parent;
209
+ const pName = parent.tagName ? parent.tagName.toLowerCase() : '';
210
+ if (pName) {
211
+ if (tagName === '*' && registry[pName]) return parent;
212
+ if (pName === tagName.toLowerCase()) return parent;
211
213
  }
212
214
  parent = parent.parentElement || (parent.getRootNode && parent.getRootNode().host);
213
215
  }
@@ -274,8 +276,8 @@ const Lego = (() => {
274
276
  }
275
277
  };
276
278
 
277
- const bind = (container, componentRoot, loopCtx = null) => {
278
- const state = componentRoot._studs;
279
+ const bind = (container, blockRoot, loopCtx = null) => {
280
+ const state = blockRoot._studs;
279
281
 
280
282
  const bindNode = (child) => {
281
283
  const childData = getPrivateData(child);
@@ -291,7 +293,7 @@ const Lego = (() => {
291
293
  try {
292
294
  let evalScope = state;
293
295
  if (loopCtx) {
294
- const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
296
+ const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
295
297
  const item = list[loopCtx.index];
296
298
  evalScope = Object.assign(Object.create(state), { [loopCtx.name]: item });
297
299
  }
@@ -309,7 +311,7 @@ const Lego = (() => {
309
311
  try {
310
312
  let target, last;
311
313
  if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
312
- const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
314
+ const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
313
315
  const item = list[loopCtx.index];
314
316
  if (!item) return;
315
317
  const subPath = prop.split('.').slice(1);
@@ -357,7 +359,7 @@ const Lego = (() => {
357
359
  let curr = n.parentNode;
358
360
  while (curr && curr !== container) {
359
361
  if (curr.hasAttribute && curr.hasAttribute('b-for')) return true;
360
- // Only stop at Shadow Roots or explicit boundaries, NOT component tags in Light DOM
362
+ // Only stop at Shadow Roots or explicit boundaries, NOT block tags in Light DOM
361
363
  // The parent MUST be able to bind data to the slots of its children.
362
364
  curr = curr.parentNode;
363
365
  }
@@ -462,7 +464,7 @@ const Lego = (() => {
462
464
  if (config.metrics && config.metrics.onRenderStart) config.metrics.onRenderStart(el);
463
465
 
464
466
  try {
465
- // Use shadowRoot if it's a component, otherwise render the element itself (light DOM)
467
+ // Use shadowRoot if it's a block, otherwise render the element itself (light DOM)
466
468
  const target = el.shadowRoot || el;
467
469
  if (!data.bindings) data.bindings = scanForBindings(target);
468
470
 
@@ -525,9 +527,9 @@ const Lego = (() => {
525
527
  }
526
528
  });
527
529
 
528
- // Global Broadcast: Only notify components that depend on globals
530
+ // Global Broadcast: Only notify blocks that depend on globals
529
531
  if (state === Lego.globals) {
530
- activeComponents.forEach(comp => {
532
+ activeBlocks.forEach(comp => {
531
533
  if (getPrivateData(comp).hasGlobalDependency) render(comp);
532
534
  });
533
535
  }
@@ -558,12 +560,16 @@ const Lego = (() => {
558
560
  }
559
561
  }
560
562
 
561
- // TIER 1: Logic from Lego.define (SFC)
563
+ // TIER 1: Logic from Lego.block (Lego File)
562
564
  // TIER 2: Logic from the <template b-data="..."> attribute
563
565
  // TIER 3: Logic from the <my-comp b-data="..."> tag
564
- const scriptLogic = sfcLogic.get(name) || {};
565
- const templateLogic = parseJSObject(templateNode.getAttribute('b-data') || '{}');
566
- const instanceLogic = parseJSObject(el.getAttribute('b-data') || '{}');
566
+ // SCOPED PROPS: Use parent block state as scope for instance logic evaluation
567
+ const parentBlock = findAncestor(el, '*') || findAncestor(el.getRootNode().host, '*');
568
+ const parentScope = parentBlock && parentBlock.state ? parentBlock.state : {};
569
+
570
+ const scriptLogic = legoFileLogic.get(name) || {};
571
+ const templateLogic = parseJSObject(templateNode.getAttribute('b-logic') || templateNode.getAttribute('b-data') || '{}');
572
+ const instanceLogic = parseJSObject(el.getAttribute('b-logic') || el.getAttribute('b-data') || '{}', parentScope);
567
573
 
568
574
  el._studs = reactive({
569
575
  ...scriptLogic,
@@ -571,6 +577,7 @@ const Lego = (() => {
571
577
  ...instanceLogic,
572
578
  $vars: {},
573
579
  $element: el,
580
+ get $parent() { return findAncestor(el, '*') },
574
581
  $emit: (name, detail) => {
575
582
  el.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
576
583
  },
@@ -593,7 +600,7 @@ const Lego = (() => {
593
600
  }
594
601
 
595
602
  bind(shadow, el);
596
- activeComponents.add(el);
603
+ activeBlocks.add(el);
597
604
  render(el);
598
605
 
599
606
  [...shadow.children].forEach(snap);
@@ -619,7 +626,7 @@ const Lego = (() => {
619
626
  [...el.shadowRoot.children].forEach(unsnap);
620
627
  }
621
628
 
622
- activeComponents.delete(el);
629
+ activeBlocks.delete(el);
623
630
  [...el.children].forEach(unsnap);
624
631
  };
625
632
 
@@ -701,7 +708,7 @@ const Lego = (() => {
701
708
  snap(n);
702
709
  // Auto-Discovery: Check if tag is unknown and loader is configured
703
710
  const tagName = n.tagName.toLowerCase();
704
- if (tagName.includes('-') && !registry[tagName] && config.loader && !activeComponents.has(n)) {
711
+ if (tagName.includes('-') && !registry[tagName] && config.loader && !activeBlocks.has(n)) {
705
712
  const result = config.loader(tagName);
706
713
  if (result) {
707
714
  // Handle Promise (user does custom fetch) vs String (we fetch)
@@ -710,7 +717,7 @@ const Lego = (() => {
710
717
  : result;
711
718
 
712
719
  Promise.resolve(promise)
713
- .then(sfc => publicAPI.defineSFC(sfc, tagName + '.lego'))
720
+ .then(legoFile => publicAPI.defineLegoFile(legoFile, tagName + '.lego'))
714
721
  .catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
715
722
  }
716
723
  }
@@ -774,7 +781,7 @@ const Lego = (() => {
774
781
  },
775
782
  $go: (path, ...targets) => _go(path, ...targets)(document.body)
776
783
  }, document.body),
777
- defineSFC: (content, filename = 'component.lego') => {
784
+ defineLegoFile: (content, filename = 'block.lego') => {
778
785
  let template = '';
779
786
  let script = '{}';
780
787
  let stylesAttr = '';
@@ -820,7 +827,7 @@ const Lego = (() => {
820
827
  remaining = remaining.slice(contentEnd + closeTag.length);
821
828
  }
822
829
 
823
- const name = deriveComponentName(filename);
830
+ const name = deriveBlockName(filename);
824
831
  // We must eval the script to get the object.
825
832
  // Safe-ish because it's coming from the "Server" (trusted source in this architecture)
826
833
  const logicObj = new Function(`return ${script}`)();
@@ -832,18 +839,18 @@ const Lego = (() => {
832
839
  registry[name] = document.createElement('template');
833
840
  registry[name].innerHTML = template;
834
841
  registry[name].setAttribute('b-stylesheets', stylesAttr);
835
- sfcLogic.set(name, logicObj);
842
+ legoFileLogic.set(name, logicObj);
836
843
 
837
844
  // Upgrade existing elements
838
845
  document.querySelectorAll(name).forEach(el => !getPrivateData(el).snapped && snap(el));
839
846
  },
840
- define: (tagName, templateHTML, logic = {}, styles = "") => {
847
+ block: (tagName, templateHTML, logic = {}, styles = "") => {
841
848
  const t = document.createElement('template');
842
849
  t.setAttribute('b-id', tagName);
843
850
  t.setAttribute('b-stylesheets', styles);
844
851
  t.innerHTML = templateHTML;
845
852
  registry[tagName] = t;
846
- sfcLogic.set(tagName, logic);
853
+ legoFileLogic.set(tagName, logic);
847
854
 
848
855
  // Initialize shared state with try-catch safety
849
856
  try {
@@ -854,8 +861,11 @@ const Lego = (() => {
854
861
 
855
862
  document.querySelectorAll(tagName).forEach(snap);
856
863
  },
864
+ // Alias for backward compatibility
865
+ get define() { return this.block },
866
+
857
867
  // For specific test validation
858
- getActiveComponentsCount: () => activeComponents.size,
868
+ getActiveBlocksCount: () => activeBlocks.size,
859
869
  getLegos: () => Object.keys(registry),
860
870
  config, // Expose config for customization
861
871
  route: (path, tagName, middleware = null) => {
package/main.min.js CHANGED
@@ -1,7 +1,13 @@
1
- const Lego=(()=>{const b={},M=new WeakMap,O=new WeakMap,P=new WeakMap,v=new Set,D=new Map,B=new Map,H=new Map,j=new Map;let q={};const p={onError:(e,s,c)=>{console.error(`[Lego Error] [${s}]`,e,c)},metrics:{},syntax:"brackets"},$=()=>p.syntax==="brackets"?["[[","]]"]:["{{","}}"],L=()=>{const[e,s]=$(),c=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),o=s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`${c}(.*?)${o}`,"g")},R=[],ee=e=>typeof e!="string"?e:e.replace(/[&<>"']/g,s=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[s]),K=e=>{const c=e.split("/").pop().replace(/\.lego$/,"").replace(/_/g,"-").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();if(!c.includes("-"))throw new Error(`[Lego] Invalid component definition: "${e}". Component names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`);return c},V=(e,s)=>{if(typeof e=="function"){const o=Array.from(document.querySelectorAll("*")).filter(t=>t.tagName.includes("-"));return[].concat(e(o))}if(e.startsWith("#")){const o=document.getElementById(e.slice(1));return o?[o]:[]}const c=s?.querySelectorAll(e)||[];return c.length>0?[...c]:[...document.querySelectorAll(e)]},G=(e,...s)=>c=>{const o=async(t,a=null,r=!0,n={})=>{if(r){const d={legoTargets:s.filter(i=>typeof i=="string"),method:t,body:a};history.pushState(d,"",e)}await W(s.length?s:null,c)};return{get:(t=!0,a={})=>o("GET",null,t,a),post:(t,a=!0,r={})=>o("POST",t,a,r),put:(t,a=!0,r={})=>o("PUT",t,a,r),patch:(t,a=!0,r={})=>o("PATCH",t,a,r),delete:(t=!0,a={})=>o("DELETE",null,t,a)}},J=(()=>{let e=!1;const s=new Set;let c=!1,o=null;const t=new Set,a=()=>{o&&clearTimeout(o),o=setTimeout(()=>{t.forEach(r=>{const n=r._studs;if(n&&typeof n.updated=="function")try{n.updated.call(n)}catch(l){console.error("[Lego] Error in updated hook:",l)}}),t.clear(),o=null},50)};return{add:r=>{!r||c||(s.add(r),!e&&(e=!0,requestAnimationFrame(()=>{c=!0;const n=Array.from(s);s.clear(),e=!1,n.forEach(l=>x(l)),n.forEach(l=>t.add(l)),a(),c=!1})))}}})(),S=(e,s,c=J)=>{if(e===null||typeof e!="object"||e instanceof Node)return e;if(M.has(e))return M.get(e);const o={get:(a,r)=>{const n=Reflect.get(a,r);return n!==null&&typeof n=="object"&&!(n instanceof Node)?S(n,s,c):n},set:(a,r,n)=>{const l=a[r],d=Reflect.set(a,r,n);return l!==n&&c.add(s),d},deleteProperty:(a,r)=>{const n=Reflect.deleteProperty(a,r);return c.add(s),n}},t=new Proxy(e,o);return M.set(e,t),t},U=e=>{try{return new Function(`return (${e})`)()}catch(s){return console.error("[Lego] Failed to parse b-data:",e,s),{}}},N=e=>(O.has(e)||O.set(e,{snapped:!1,bindings:null,bound:!1,rendering:!1,anchor:null,hasGlobalDependency:!1}),O.get(e)),F=(e,s)=>{if(!e)return"";const c=e.trim().split(".");let o=s;for(const t of c){if(o==null)return"";o=o[t]}return o??""},Z=(e,s)=>{let c=e.parentElement||e.getRootNode().host;for(;c;){if(c.tagName&&c.tagName.toLowerCase()===s.toLowerCase())return c;c=c.parentElement||c.getRootNode&&c.getRootNode().host}},h=(e,s,c=!1)=>{if(/\b(function|eval|import|class|module|deploy|constructor|__proto__)\b/.test(e)){console.warn(`[Lego] Security Warning: Blocked dangerous expression "${e}"`);return}try{const o=s.state||{};let t=H.get(e);t||(t=new Function("global","self","event","helpers",`
1
+ const Lego = (() => {
2
+ const b = {}, M = new WeakMap, O = new WeakMap, P = new WeakMap, v = new Set, D = new Map, B = new Map, H = new Map, j = new Map; let q = {}; const p = { onError: (e, s, c) => { console.error(`[Lego Error] [${s}]`, e, c) }, metrics: {}, syntax: "brackets" }, $ = () => p.syntax === "brackets" ? ["[[", "]]"] : ["{{", "}}"], L = () => { const [e, s] = $(), c = e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), o = s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return new RegExp(`${c}(.*?)${o}`, "g") }, R = [], ee = e => typeof e != "string" ? e : e.replace(/[&<>"']/g, s => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[s]), K = e => { const c = e.split("/").pop().replace(/\.lego$/, "").replace(/_/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); if (!c.includes("-")) throw new Error(`[Lego] Invalid component definition: "${e}". Component names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`); return c }, V = (e, s) => { if (typeof e == "function") { const o = Array.from(document.querySelectorAll("*")).filter(t => t.tagName.includes("-")); return [].concat(e(o)) } if (e.startsWith("#")) { const o = document.getElementById(e.slice(1)); return o ? [o] : [] } const c = s?.querySelectorAll(e) || []; return c.length > 0 ? [...c] : [...document.querySelectorAll(e)] }, G = (e, ...s) => c => { const o = async (t, a = null, r = !0, n = {}) => { if (r) { const d = { legoTargets: s.filter(i => typeof i == "string"), method: t, body: a }; history.pushState(d, "", e) } await W(s.length ? s : null, c) }; return { get: (t = !0, a = {}) => o("GET", null, t, a), post: (t, a = !0, r = {}) => o("POST", t, a, r), put: (t, a = !0, r = {}) => o("PUT", t, a, r), patch: (t, a = !0, r = {}) => o("PATCH", t, a, r), delete: (t = !0, a = {}) => o("DELETE", null, t, a) } }, J = (() => { let e = !1; const s = new Set; let c = !1, o = null; const t = new Set, a = () => { o && clearTimeout(o), o = setTimeout(() => { t.forEach(r => { const n = r._studs; if (n && typeof n.updated == "function") try { n.updated.call(n) } catch (l) { console.error("[Lego] Error in updated hook:", l) } }), t.clear(), o = null }, 50) }; return { add: r => { !r || c || (s.add(r), !e && (e = !0, requestAnimationFrame(() => { c = !0; const n = Array.from(s); s.clear(), e = !1, n.forEach(l => x(l)), n.forEach(l => t.add(l)), a(), c = !1 }))) } } })(), S = (e, s, c = J) => { if (e === null || typeof e != "object" || e instanceof Node) return e; if (M.has(e)) return M.get(e); const o = { get: (a, r) => { const n = Reflect.get(a, r); return n !== null && typeof n == "object" && !(n instanceof Node) ? S(n, s, c) : n }, set: (a, r, n) => { const l = a[r], d = Reflect.set(a, r, n); return l !== n && c.add(s), d }, deleteProperty: (a, r) => { const n = Reflect.deleteProperty(a, r); return c.add(s), n } }, t = new Proxy(e, o); return M.set(e, t), t }, U = e => { try { return new Function(`return (${e})`)() } catch (s) { return console.error("[Lego] Failed to parse b-data:", e, s), {} } }, N = e => (O.has(e) || O.set(e, { snapped: !1, bindings: null, bound: !1, rendering: !1, anchor: null, hasGlobalDependency: !1 }), O.get(e)), F = (e, s) => { if (!e) return ""; const c = e.trim().split("."); let o = s; for (const t of c) { if (o == null) return ""; o = o[t] } return o ?? "" }, Z = (e, s) => { let c = e.parentElement || e.getRootNode().host; for (; c;) { if (c.tagName && c.tagName.toLowerCase() === s.toLowerCase()) return c; c = c.parentElement || c.getRootNode && c.getRootNode().host } }, h = (e, s, c = !1) => {
3
+ if (/\b(function|eval|import|class|module|deploy|constructor|__proto__)\b/.test(e)) { console.warn(`[Lego] Security Warning: Blocked dangerous expression "${e}"`); return } try {
4
+ const o = s.state || {}; let t = H.get(e); t || (t = new Function("global", "self", "event", "helpers", `
2
5
  with(helpers) {
3
6
  with(this) {
4
7
  return ${e}
5
8
  }
6
9
  }
7
- `),H.set(e,t));const a={$ancestors:n=>Z(s.self,n),$registry:n=>B.get(n.toLowerCase()),$element:s.self,$route:Lego.globals.$route,$go:(n,...l)=>G(n,...l)(s.self),$emit:(n,l)=>{s.self.dispatchEvent(new CustomEvent(n,{detail:l,bubbles:!0,composed:!0}))}},r=t.call(o,s.global,s.self,s.event,a);return typeof r=="function"?r.call(o,s.event):r}catch(o){if(c)throw o;p.onError(o,"render-error",s.self);return}},I=(e,s)=>{if(e.type==="checkbox")e.checked!==!!s&&(e.checked=!!s);else{const c=s==null?"":String(s);e.value!==c&&(e.value=c)}},C=(e,s,c=null)=>{const o=s._studs,t=n=>{const l=N(n);if(!l.bound){if(n.hasAttributes()){const d=n.attributes;for(let i=0;i<d.length;i++){const u=d[i];if(u.name.startsWith("@")){const f=u.name.slice(1);n.addEventListener(f,g=>{try{let m=o;if(c){const E=h(c.listName,{state:o,global:Lego.globals,self:s})[c.index];m=Object.assign(Object.create(o),{[c.name]:E})}h(u.value,{state:m,global:Lego.globals,self:n,event:g},!0)}catch(m){p.onError(m,"event-handler",n)}})}}if(n.hasAttribute("b-sync")){const i=n.getAttribute("b-sync"),u=()=>{try{let f,g;if(c&&i.startsWith(c.name+".")){const E=h(c.listName,{state:o,global:Lego.globals,self:s})[c.index];if(!E)return;const w=i.split(".").slice(1);g=w.pop(),f=w.reduce((T,_)=>T[_],E)}else{const y=i.split(".");g=y.pop(),f=y.reduce((E,w)=>E[w],o)}const m=n.type==="checkbox"?n.checked:n.value;f&&f[g]!==m&&(f[g]=m)}catch(f){p.onError(f,"sync-update",n)}};n.addEventListener("input",u),n.addEventListener("change",u)}if(n.hasAttribute("b-var")){const i=n.getAttribute("b-var");o.$vars&&(o.$vars[i]=n)}}l.bound=!0}};e instanceof Element&&t(e);const a=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT);let r;for(;r=a.nextNode();)t(r)},Y=e=>{const s=[],c=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT);let o;for(;o=c.nextNode();){if((r=>{let n=r.parentNode;for(;n&&n!==e;){if(n.hasAttribute&&n.hasAttribute("b-for"))return!0;n=n.parentNode}return!1})(o))continue;const a=r=>{if(/\bglobal\b/.test(r)){const n=e.host||e;N(n).hasGlobalDependency=!0}};if(o.nodeType===Node.ELEMENT_NODE){if(o.hasAttribute("b-if")){const n=o.getAttribute("b-if");a(n);const l=document.createComment(`b-if: ${n}`),d=N(o);d.anchor=l,s.push({type:"b-if",node:o,anchor:l,expr:n})}if(o.hasAttribute("b-show")){const n=o.getAttribute("b-show");a(n),s.push({type:"b-show",node:o,expr:n})}if(o.hasAttribute("b-for")){const n=o.getAttribute("b-for").match(/^\s*(\w+)\s+in\s+([\s\S]+?)\s*$/);n&&(a(n[2]),s.push({type:"b-for",node:o,itemName:n[1],listName:n[2].trim(),template:o.cloneNode(!0)}),o.innerHTML="")}if(o.hasAttribute("b-text")&&s.push({type:"b-text",node:o,path:o.getAttribute("b-text")}),o.hasAttribute("b-html")){const n=o.getAttribute("b-html");a(n),s.push({type:"b-html",node:o,expr:n})}o.hasAttribute("b-sync")&&s.push({type:"b-sync",node:o});const[r]=$();[...o.attributes].forEach(n=>{n.value.includes(r)&&(a(n.value),s.push({type:"attr",node:o,attrName:n.name,template:n.value}))})}else if(o.nodeType===Node.TEXT_NODE){const[r]=$();o.textContent.includes(r)&&(a(o.textContent),s.push({type:"text",node:o,template:o.textContent}))}}return s},Q=(e,s)=>{const c=a=>{if(a.nodeType===Node.TEXT_NODE){a._tpl===void 0&&(a._tpl=a.textContent);const r=a._tpl.replace(L(),(n,l)=>h(l.trim(),{state:s,global:Lego.globals,self:a})??"");a.textContent!==r&&(a.textContent=r)}else if(a.nodeType===Node.ELEMENT_NODE){const[r]=$();[...a.attributes].forEach(n=>{if(n._tpl===void 0&&(n._tpl=n.value),n._tpl.includes(r)){const l=n._tpl.replace(L(),(d,i)=>h(i.trim(),{state:s,global:Lego.globals,self:a})??"");n.value!==l&&(n.value=l,n.name==="class"&&(a.className=l))}})}};c(e);const o=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT);let t;for(;t=o.nextNode();)c(t)},x=e=>{const s=e._studs;if(!s)return;const c=N(e);if(!c.rendering){c.rendering=!0,p.metrics&&p.metrics.onRenderStart&&p.metrics.onRenderStart(e);try{const o=e.shadowRoot||e;c.bindings||(c.bindings=Y(o)),c.bindings.forEach(t=>{if(t.type==="b-if"){const a=!!h(t.expr,{state:s,global:Lego.globals,self:t.node}),r=!!t.node.parentNode;a&&!r?t.anchor.parentNode&&t.anchor.parentNode.replaceChild(t.node,t.anchor):!a&&r&&t.node.parentNode.replaceChild(t.anchor,t.node)}if(t.type==="b-show"&&(t.node.style.display=h(t.expr,{state:s,global:Lego.globals,self:t.node})?"":"none"),t.type==="b-text"&&(t.node.textContent=F(t.path,s)),t.type==="b-html"&&(t.node.innerHTML=h(t.expr,{state:s,global:Lego.globals,self:t.node})||""),t.type==="b-sync"&&I(t.node,F(t.node.getAttribute("b-sync"),s)),t.type==="text"){const a=t.template.replace(L(),(r,n)=>h(n.trim(),{state:s,global:Lego.globals,self:t.node})??"");t.node.textContent!==a&&(t.node.textContent=a)}if(t.type==="attr"){const a=t.template.replace(L(),(r,n)=>h(n.trim(),{state:s,global:Lego.globals,self:t.node})??"");t.node.getAttribute(t.attrName)!==a&&(t.node.setAttribute(t.attrName,a),t.attrName==="class"&&(t.node.className=a))}if(t.type==="b-for"){const a=h(t.listName,{state:s,global:Lego.globals,self:e})||[];P.has(t.node)||P.set(t.node,new Map);const r=P.get(t.node),n=new Set;a.forEach((l,d)=>{const i=l&&typeof l=="object"?l.__id||(l.__id=Math.random()):`${d}-${l}`;n.add(i);let u=r.get(i);u||(u=t.template.cloneNode(!0),u.removeAttribute("b-for"),r.set(i,u),C(u,e,{name:t.itemName,listName:t.listName,index:d}));const f=Object.assign(Object.create(s),{[t.itemName]:l});Q(u,f),u.querySelectorAll("[b-sync]").forEach(g=>{const m=g.getAttribute("b-sync");if(m.startsWith(t.itemName+".")){const y=h(t.listName,{state:s,global:Lego.globals,self:e});I(g,F(m.split(".").slice(1).join("."),y[d]))}}),t.node.children[d]!==u&&t.node.insertBefore(u,t.node.children[d]||null)});for(const[l,d]of r.entries())n.has(l)||(d.remove(),r.delete(l))}}),s===Lego.globals&&v.forEach(t=>{N(t).hasGlobalDependency&&x(t)})}catch(o){p.onError(o,"render",e)}finally{p.metrics&&p.metrics.onRenderEnd&&p.metrics.onRenderEnd(e),c.rendering=!1}}},A=e=>{if(!e||e.nodeType!==Node.ELEMENT_NODE)return;const s=N(e),c=e.tagName.toLowerCase(),o=b[c];if(o&&!s.snapped){s.snapped=!0;const a=o.content.cloneNode(!0),r=e.attachShadow({mode:"open"}),n=(o.getAttribute("b-stylesheets")||"").split(/\s+/).filter(Boolean);if(n.length>0){const f=n.flatMap(g=>j.get(g)||[]);f.length>0&&(r.adoptedStyleSheets=[...f])}const l=D.get(c)||{},d=U(o.getAttribute("b-data")||"{}"),i=U(e.getAttribute("b-data")||"{}");e._studs=S({...l,...d,...i,$vars:{},$element:e,$emit:(f,g)=>{e.dispatchEvent(new CustomEvent(f,{detail:g,bubbles:!0,composed:!0}))},get $route(){return Lego.globals.$route},get $go(){return Lego.globals.$go}},e),Object.defineProperty(e,"state",{get(){return this._studs},set(f){Object.assign(this._studs,f)},configurable:!0,enumerable:!1}),r.appendChild(a);const u=r.querySelector("style");if(u&&(u.textContent=u.textContent.replace(/\bself\b/g,":host")),C(r,e),v.add(e),x(e),[...r.children].forEach(A),typeof e._studs.mounted=="function")try{e._studs.mounted.call(e._studs)}catch(f){p.onError(f,"mounted",e)}}let t=e.parentElement;for(;t&&!t._studs;)t=t.parentElement;t&&t._studs&&C(e,t),[...e.children].forEach(A)},k=e=>{if(e._studs&&typeof e._studs.unmounted=="function")try{e._studs.unmounted.call(e._studs)}catch(s){console.error("[Lego] Error in unmounted:",s)}e.shadowRoot&&[...e.shadowRoot.children].forEach(k),v.delete(e),[...e.children].forEach(k)},W=async(e=null,s=null)=>{const c=window.location.pathname,o=window.location.search,t=R.find(d=>d.regex.test(c));if(!t)return;let a=[];if(e)a=e.flatMap(d=>V(d,s));else{const d=document.querySelector("lego-router");d&&(a=[d])}if(a.length===0)return;const r=c.match(t.regex).slice(1),n=Object.fromEntries(t.paramNames.map((d,i)=>[d,r[i]])),l=Object.fromEntries(new URLSearchParams(o));t.middleware&&!await t.middleware(n,Lego.globals)||(Lego.globals.$route.url=c+o,Lego.globals.$route.route=t.path,Lego.globals.$route.params=n,Lego.globals.$route.query=l,Lego.globals.$route.method=history.state?.method||"GET",Lego.globals.$route.body=history.state?.body||null,a.forEach(d=>{if(d){const i=document.createElement(t.tagName);d.replaceChildren(i)}}))},z={snap:A,unsnap:k,init:async(e=document.body,s={})=>{(!e||typeof e.nodeType!="number")&&(e=document.body),q=s.styles||{},p.loader=s.loader;const c=Object.entries(q).map(async([t,a])=>{const r=await Promise.all(a.map(async n=>{try{const d=await(await fetch(n)).text(),i=new CSSStyleSheet;return await i.replace(d),i}catch(l){return console.error(`[Lego] Failed to load stylesheet: ${n}`,l),null}}));j.set(t,r.filter(n=>n!==null))});if(await Promise.all(c),document.querySelectorAll("template[b-id]").forEach(t=>{b[t.getAttribute("b-id")]=t}),new MutationObserver(t=>t.forEach(a=>{a.addedNodes.forEach(r=>{if(r.nodeType===Node.ELEMENT_NODE){A(r);const n=r.tagName.toLowerCase();if(n.includes("-")&&!b[n]&&p.loader&&!v.has(r)){const l=p.loader(n);if(l){const d=typeof l=="string"?fetch(l).then(i=>i.text()):l;Promise.resolve(d).then(i=>z.defineSFC(i,n+".lego")).catch(i=>console.error(`[Lego] Failed to load ${n}:`,i))}}}}),a.removedNodes.forEach(r=>r.nodeType===Node.ELEMENT_NODE&&k(r))})).observe(e,{childList:!0,subtree:!0}),e._studs=Lego.globals,A(e),C(e,e),x(e),s.studio){if(!b["lego-studio"]){const t=document.createElement("script");t.src="https://unpkg.com/@legodom/studio@0.0.2/dist/lego-studio.js",t.onerror=()=>console.warn("[Lego] Failed to load Studio from CDN"),document.head.appendChild(t)}Lego.route("/_/studio","lego-studio"),Lego.route("/_/studio/:component","lego-studio")}R.length>0&&(window.addEventListener("popstate",t=>{const a=t.state?.legoTargets||null;W(a)}),document.addEventListener("submit",t=>{t.preventDefault()}),document.addEventListener("click",t=>{const r=t.composedPath().find(n=>n.tagName==="A"&&(n.hasAttribute("b-target")||n.hasAttribute("b-link")));if(r){t.preventDefault();const n=r.getAttribute("href"),l=r.getAttribute("b-target"),d=l?l.split(/\s+/).filter(Boolean):[],i=r.getAttribute("b-link")!=="false";Lego.globals.$go(n,...d).get(i)}}),W())},globals:S({$route:{url:window.location.pathname,route:"",params:{},query:{},method:"GET",body:null},$go:(e,...s)=>G(e,...s)(document.body)},document.body),defineSFC:(e,s="component.lego")=>{let c="",o="{}",t="",a="",r=e;const n=/<(template|script|style)\b((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)*)>/i;for(;r;){const i=r.match(n);if(!i)break;const u=i[1].toLowerCase(),f=i[2],g=i[0],m=i.index,y=`</${u}>`,E=m+g.length,w=r.indexOf(y,E);if(w===-1){console.warn(`[Lego] Unclosed <${u}> tag in ${s}`);break}const T=r.slice(E,w);if(u==="template"){c=T.trim();const _=f.match(/b-stylesheets=["']([^"']+)["']/);_&&(t=_[1])}else if(u==="script"){const _=T.trim(),X=_.match(/export\s+default\s+({[\s\S]*})/);o=X?X[1]:_}else u==="style"&&(a=T.trim());r=r.slice(w+y.length)}const l=K(s),d=new Function(`return ${o}`)();a&&(c=`<style>${a}</style>`+c),b[l]=document.createElement("template"),b[l].innerHTML=c,b[l].setAttribute("b-stylesheets",t),D.set(l,d),document.querySelectorAll(l).forEach(i=>!N(i).snapped&&A(i))},define:(e,s,c={},o="")=>{const t=document.createElement("template");t.setAttribute("b-id",e),t.setAttribute("b-stylesheets",o),t.innerHTML=s,b[e]=t,D.set(e,c);try{B.set(e.toLowerCase(),S({...c},document.body))}catch(a){p.onError(a,"define",e)}document.querySelectorAll(e).forEach(A)},getActiveComponentsCount:()=>v.size,getLegos:()=>Object.keys(b),config:p,route:(e,s,c=null)=>{const o=[],t=e.replace(/:([^\/]+)/g,(a,r)=>(o.push(r),"([^/]+)"));R.push({path:e,regex:new RegExp(`^${t}$`),tagName:s,paramNames:o,middleware:c})}};return z})();typeof window<"u"&&(window.Lego=Lego);
10
+ `), H.set(e, t)); const a = { $ancestors: n => Z(s.self, n), $registry: n => B.get(n.toLowerCase()), $element: s.self, $route: Lego.globals.$route, $go: (n, ...l) => G(n, ...l)(s.self), $emit: (n, l) => { s.self.dispatchEvent(new CustomEvent(n, { detail: l, bubbles: !0, composed: !0 })) } }, r = t.call(o, s.global, s.self, s.event, a); return typeof r == "function" ? r.call(o, s.event) : r
11
+ } catch (o) { if (c) throw o; p.onError(o, "render-error", s.self); return }
12
+ }, I = (e, s) => { if (e.type === "checkbox") e.checked !== !!s && (e.checked = !!s); else { const c = s == null ? "" : String(s); e.value !== c && (e.value = c) } }, C = (e, s, c = null) => { const o = s._studs, t = n => { const l = N(n); if (!l.bound) { if (n.hasAttributes()) { const d = n.attributes; for (let i = 0; i < d.length; i++) { const u = d[i]; if (u.name.startsWith("@")) { const f = u.name.slice(1); n.addEventListener(f, g => { try { let m = o; if (c) { const E = h(c.listName, { state: o, global: Lego.globals, self: s })[c.index]; m = Object.assign(Object.create(o), { [c.name]: E }) } h(u.value, { state: m, global: Lego.globals, self: n, event: g }, !0) } catch (m) { p.onError(m, "event-handler", n) } }) } } if (n.hasAttribute("b-sync")) { const i = n.getAttribute("b-sync"), u = () => { try { let f, g; if (c && i.startsWith(c.name + ".")) { const E = h(c.listName, { state: o, global: Lego.globals, self: s })[c.index]; if (!E) return; const w = i.split(".").slice(1); g = w.pop(), f = w.reduce((T, _) => T[_], E) } else { const y = i.split("."); g = y.pop(), f = y.reduce((E, w) => E[w], o) } const m = n.type === "checkbox" ? n.checked : n.value; f && f[g] !== m && (f[g] = m) } catch (f) { p.onError(f, "sync-update", n) } }; n.addEventListener("input", u), n.addEventListener("change", u) } if (n.hasAttribute("b-var")) { const i = n.getAttribute("b-var"); o.$vars && (o.$vars[i] = n) } } l.bound = !0 } }; e instanceof Element && t(e); const a = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT); let r; for (; r = a.nextNode();)t(r) }, Y = e => { const s = [], c = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); let o; for (; o = c.nextNode();) { if ((r => { let n = r.parentNode; for (; n && n !== e;) { if (n.hasAttribute && n.hasAttribute("b-for")) return !0; n = n.parentNode } return !1 })(o)) continue; const a = r => { if (/\bglobal\b/.test(r)) { const n = e.host || e; N(n).hasGlobalDependency = !0 } }; if (o.nodeType === Node.ELEMENT_NODE) { if (o.hasAttribute("b-if")) { const n = o.getAttribute("b-if"); a(n); const l = document.createComment(`b-if: ${n}`), d = N(o); d.anchor = l, s.push({ type: "b-if", node: o, anchor: l, expr: n }) } if (o.hasAttribute("b-show")) { const n = o.getAttribute("b-show"); a(n), s.push({ type: "b-show", node: o, expr: n }) } if (o.hasAttribute("b-for")) { const n = o.getAttribute("b-for").match(/^\s*(\w+)\s+in\s+([\s\S]+?)\s*$/); n && (a(n[2]), s.push({ type: "b-for", node: o, itemName: n[1], listName: n[2].trim(), template: o.cloneNode(!0) }), o.innerHTML = "") } if (o.hasAttribute("b-text") && s.push({ type: "b-text", node: o, path: o.getAttribute("b-text") }), o.hasAttribute("b-html")) { const n = o.getAttribute("b-html"); a(n), s.push({ type: "b-html", node: o, expr: n }) } o.hasAttribute("b-sync") && s.push({ type: "b-sync", node: o }); const [r] = $();[...o.attributes].forEach(n => { n.value.includes(r) && (a(n.value), s.push({ type: "attr", node: o, attrName: n.name, template: n.value })) }) } else if (o.nodeType === Node.TEXT_NODE) { const [r] = $(); o.textContent.includes(r) && (a(o.textContent), s.push({ type: "text", node: o, template: o.textContent })) } } return s }, Q = (e, s) => { const c = a => { if (a.nodeType === Node.TEXT_NODE) { a._tpl === void 0 && (a._tpl = a.textContent); const r = a._tpl.replace(L(), (n, l) => h(l.trim(), { state: s, global: Lego.globals, self: a }) ?? ""); a.textContent !== r && (a.textContent = r) } else if (a.nodeType === Node.ELEMENT_NODE) { const [r] = $();[...a.attributes].forEach(n => { if (n._tpl === void 0 && (n._tpl = n.value), n._tpl.includes(r)) { const l = n._tpl.replace(L(), (d, i) => h(i.trim(), { state: s, global: Lego.globals, self: a }) ?? ""); n.value !== l && (n.value = l, n.name === "class" && (a.className = l)) } }) } }; c(e); const o = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); let t; for (; t = o.nextNode();)c(t) }, x = e => { const s = e._studs; if (!s) return; const c = N(e); if (!c.rendering) { c.rendering = !0, p.metrics && p.metrics.onRenderStart && p.metrics.onRenderStart(e); try { const o = e.shadowRoot || e; c.bindings || (c.bindings = Y(o)), c.bindings.forEach(t => { if (t.type === "b-if") { const a = !!h(t.expr, { state: s, global: Lego.globals, self: t.node }), r = !!t.node.parentNode; a && !r ? t.anchor.parentNode && t.anchor.parentNode.replaceChild(t.node, t.anchor) : !a && r && t.node.parentNode.replaceChild(t.anchor, t.node) } if (t.type === "b-show" && (t.node.style.display = h(t.expr, { state: s, global: Lego.globals, self: t.node }) ? "" : "none"), t.type === "b-text" && (t.node.textContent = F(t.path, s)), t.type === "b-html" && (t.node.innerHTML = h(t.expr, { state: s, global: Lego.globals, self: t.node }) || ""), t.type === "b-sync" && I(t.node, F(t.node.getAttribute("b-sync"), s)), t.type === "text") { const a = t.template.replace(L(), (r, n) => h(n.trim(), { state: s, global: Lego.globals, self: t.node }) ?? ""); t.node.textContent !== a && (t.node.textContent = a) } if (t.type === "attr") { const a = t.template.replace(L(), (r, n) => h(n.trim(), { state: s, global: Lego.globals, self: t.node }) ?? ""); t.node.getAttribute(t.attrName) !== a && (t.node.setAttribute(t.attrName, a), t.attrName === "class" && (t.node.className = a)) } if (t.type === "b-for") { const a = h(t.listName, { state: s, global: Lego.globals, self: e }) || []; P.has(t.node) || P.set(t.node, new Map); const r = P.get(t.node), n = new Set; a.forEach((l, d) => { const i = l && typeof l == "object" ? l.__id || (l.__id = Math.random()) : `${d}-${l}`; n.add(i); let u = r.get(i); u || (u = t.template.cloneNode(!0), u.removeAttribute("b-for"), r.set(i, u), C(u, e, { name: t.itemName, listName: t.listName, index: d })); const f = Object.assign(Object.create(s), { [t.itemName]: l }); Q(u, f), u.querySelectorAll("[b-sync]").forEach(g => { const m = g.getAttribute("b-sync"); if (m.startsWith(t.itemName + ".")) { const y = h(t.listName, { state: s, global: Lego.globals, self: e }); I(g, F(m.split(".").slice(1).join("."), y[d])) } }), t.node.children[d] !== u && t.node.insertBefore(u, t.node.children[d] || null) }); for (const [l, d] of r.entries()) n.has(l) || (d.remove(), r.delete(l)) } }), s === Lego.globals && v.forEach(t => { N(t).hasGlobalDependency && x(t) }) } catch (o) { p.onError(o, "render", e) } finally { p.metrics && p.metrics.onRenderEnd && p.metrics.onRenderEnd(e), c.rendering = !1 } } }, A = e => { if (!e || e.nodeType !== Node.ELEMENT_NODE) return; const s = N(e), c = e.tagName.toLowerCase(), o = b[c]; if (o && !s.snapped) { s.snapped = !0; const a = o.content.cloneNode(!0), r = e.attachShadow({ mode: "open" }), n = (o.getAttribute("b-stylesheets") || "").split(/\s+/).filter(Boolean); if (n.length > 0) { const f = n.flatMap(g => j.get(g) || []); f.length > 0 && (r.adoptedStyleSheets = [...f]) } const l = D.get(c) || {}, d = U(o.getAttribute("b-data") || "{}"), i = U(e.getAttribute("b-data") || "{}"); e._studs = S({ ...l, ...d, ...i, $vars: {}, $element: e, $emit: (f, g) => { e.dispatchEvent(new CustomEvent(f, { detail: g, bubbles: !0, composed: !0 })) }, get $route() { return Lego.globals.$route }, get $go() { return Lego.globals.$go } }, e), Object.defineProperty(e, "state", { get() { return this._studs }, set(f) { Object.assign(this._studs, f) }, configurable: !0, enumerable: !1 }), r.appendChild(a); const u = r.querySelector("style"); if (u && (u.textContent = u.textContent.replace(/\bself\b/g, ":host")), C(r, e), v.add(e), x(e), [...r.children].forEach(A), typeof e._studs.mounted == "function") try { e._studs.mounted.call(e._studs) } catch (f) { p.onError(f, "mounted", e) } } let t = e.parentElement; for (; t && !t._studs;)t = t.parentElement; t && t._studs && C(e, t), [...e.children].forEach(A) }, k = e => { if (e._studs && typeof e._studs.unmounted == "function") try { e._studs.unmounted.call(e._studs) } catch (s) { console.error("[Lego] Error in unmounted:", s) } e.shadowRoot && [...e.shadowRoot.children].forEach(k), v.delete(e), [...e.children].forEach(k) }, W = async (e = null, s = null) => { const c = window.location.pathname, o = window.location.search, t = R.find(d => d.regex.test(c)); if (!t) return; let a = []; if (e) a = e.flatMap(d => V(d, s)); else { const d = document.querySelector("lego-router"); d && (a = [d]) } if (a.length === 0) return; const r = c.match(t.regex).slice(1), n = Object.fromEntries(t.paramNames.map((d, i) => [d, r[i]])), l = Object.fromEntries(new URLSearchParams(o)); t.middleware && !await t.middleware(n, Lego.globals) || (Lego.globals.$route.url = c + o, Lego.globals.$route.route = t.path, Lego.globals.$route.params = n, Lego.globals.$route.query = l, Lego.globals.$route.method = history.state?.method || "GET", Lego.globals.$route.body = history.state?.body || null, a.forEach(d => { if (d) { const i = document.createElement(t.tagName); d.replaceChildren(i) } })) }, z = { snap: A, unsnap: k, init: async (e = document.body, s = {}) => { (!e || typeof e.nodeType != "number") && (e = document.body), q = s.styles || {}, p.loader = s.loader; const c = Object.entries(q).map(async ([t, a]) => { const r = await Promise.all(a.map(async n => { try { const d = await (await fetch(n)).text(), i = new CSSStyleSheet; return await i.replace(d), i } catch (l) { return console.error(`[Lego] Failed to load stylesheet: ${n}`, l), null } })); j.set(t, r.filter(n => n !== null)) }); if (await Promise.all(c), document.querySelectorAll("template[b-id]").forEach(t => { b[t.getAttribute("b-id")] = t }), new MutationObserver(t => t.forEach(a => { a.addedNodes.forEach(r => { if (r.nodeType === Node.ELEMENT_NODE) { A(r); const n = r.tagName.toLowerCase(); if (n.includes("-") && !b[n] && p.loader && !v.has(r)) { const l = p.loader(n); if (l) { const d = typeof l == "string" ? fetch(l).then(i => i.text()) : l; Promise.resolve(d).then(i => z.defineLegoFile(i, n + ".lego")).catch(i => console.error(`[Lego] Failed to load ${n}:`, i)) } } } }), a.removedNodes.forEach(r => r.nodeType === Node.ELEMENT_NODE && k(r)) })).observe(e, { childList: !0, subtree: !0 }), e._studs = Lego.globals, A(e), C(e, e), x(e), s.studio) { if (!b["lego-studio"]) { const t = document.createElement("script"); t.src = "https://unpkg.com/@legodom/studio@0.0.2/dist/lego-studio.js", t.onerror = () => console.warn("[Lego] Failed to load Studio from CDN"), document.head.appendChild(t) } Lego.route("/_/studio", "lego-studio"), Lego.route("/_/studio/:component", "lego-studio") } R.length > 0 && (window.addEventListener("popstate", t => { const a = t.state?.legoTargets || null; W(a) }), document.addEventListener("submit", t => { t.preventDefault() }), document.addEventListener("click", t => { const r = t.composedPath().find(n => n.tagName === "A" && (n.hasAttribute("b-target") || n.hasAttribute("b-link"))); if (r) { t.preventDefault(); const n = r.getAttribute("href"), l = r.getAttribute("b-target"), d = l ? l.split(/\s+/).filter(Boolean) : [], i = r.getAttribute("b-link") !== "false"; Lego.globals.$go(n, ...d).get(i) } }), W()) }, globals: S({ $route: { url: window.location.pathname, route: "", params: {}, query: {}, method: "GET", body: null }, $go: (e, ...s) => G(e, ...s)(document.body) }, document.body), defineSFC: (e, s = "component.lego") => { let c = "", o = "{}", t = "", a = "", r = e; const n = /<(template|script|style)\b((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)*)>/i; for (; r;) { const i = r.match(n); if (!i) break; const u = i[1].toLowerCase(), f = i[2], g = i[0], m = i.index, y = `</${u}>`, E = m + g.length, w = r.indexOf(y, E); if (w === -1) { console.warn(`[Lego] Unclosed <${u}> tag in ${s}`); break } const T = r.slice(E, w); if (u === "template") { c = T.trim(); const _ = f.match(/b-stylesheets=["']([^"']+)["']/); _ && (t = _[1]) } else if (u === "script") { const _ = T.trim(), X = _.match(/export\s+default\s+({[\s\S]*})/); o = X ? X[1] : _ } else u === "style" && (a = T.trim()); r = r.slice(w + y.length) } const l = K(s), d = new Function(`return ${o}`)(); a && (c = `<style>${a}</style>` + c), b[l] = document.createElement("template"), b[l].innerHTML = c, b[l].setAttribute("b-stylesheets", t), D.set(l, d), document.querySelectorAll(l).forEach(i => !N(i).snapped && A(i)) }, define: (e, s, c = {}, o = "") => { const t = document.createElement("template"); t.setAttribute("b-id", e), t.setAttribute("b-stylesheets", o), t.innerHTML = s, b[e] = t, D.set(e, c); try { B.set(e.toLowerCase(), S({ ...c }, document.body)) } catch (a) { p.onError(a, "define", e) } document.querySelectorAll(e).forEach(A) }, getActiveBlocksCount: () => v.size, getLegos: () => Object.keys(b), config: p, route: (e, s, c = null) => { const o = [], t = e.replace(/:([^\/]+)/g, (a, r) => (o.push(r), "([^/]+)")); R.push({ path: e, regex: new RegExp(`^${t}$`), tagName: s, paramNames: o, middleware: c }) } }; return z
13
+ })(); typeof window < "u" && (window.Lego = Lego);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lego-dom",
3
- "version": "1.5.1",
3
+ "version": "2.0.2",
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Parser for .lego Single File Components
2
+ * Parser for .lego Single File Blocks (SFC)
3
3
  * Extracts template, script, and style sections from .lego files
4
4
  */
5
5
 
@@ -7,15 +7,15 @@
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, stylesAttr: string, componentName: string}}
10
+ * @returns {{template: string, script: string, style: string, stylesAttr: string, blockName: string}}
11
11
  */
12
- export function parseLego(content, filename = 'component.lego') {
12
+ export function parseLego(content, filename = 'block.lego') {
13
13
  const result = {
14
14
  template: '',
15
15
  script: '',
16
16
  style: '',
17
17
  stylesAttr: '',
18
- componentName: deriveComponentName(filename)
18
+ blockName: deriveBlockName(filename)
19
19
  };
20
20
 
21
21
  let remaining = content;
@@ -75,23 +75,23 @@ export function parseLego(content, filename = 'component.lego') {
75
75
  }
76
76
 
77
77
  /**
78
- * Derive component name from filename
79
- * e.g., "sample-component.lego" -> "sample-component"
78
+ * Derive block name from filename
79
+ * e.g., "sample-block.lego" -> "sample-block"
80
80
  * @param {string} filename
81
81
  * @returns {string}
82
82
  */
83
- export function deriveComponentName(filename) {
83
+ export function deriveBlockName(filename) {
84
84
  const basename = filename.split('/').pop();
85
85
  return basename.replace(/\.lego$/, '');
86
86
  }
87
87
 
88
88
  /**
89
- * Generate Lego.define() code from parsed .lego file
89
+ * Generate Lego.block() code from parsed .lego file
90
90
  * @param {object} parsed - Parsed .lego file object
91
91
  * @returns {string} - JavaScript code string
92
92
  */
93
- export function generateDefineCall(parsed) {
94
- const { componentName, template, script, style, stylesAttr } = parsed;
93
+ export function generateBlockCall(parsed) {
94
+ const { blockName, template, script, style, stylesAttr } = parsed;
95
95
 
96
96
  // Build template HTML
97
97
  let templateHTML = '';
@@ -115,8 +115,8 @@ export function generateDefineCall(parsed) {
115
115
  }
116
116
  }
117
117
 
118
- // Generate the Lego.define call
119
- return `Lego.define('${componentName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode}, '${stylesAttr}');`;
118
+ // Generate the Lego.block call
119
+ return `Lego.block('${blockName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode}, '${stylesAttr}');`;
120
120
  }
121
121
 
122
122
  /**
@@ -137,15 +137,15 @@ export function validateLego(parsed) {
137
137
  const errors = [];
138
138
 
139
139
  if (!parsed.template && !parsed.script && !parsed.style) {
140
- errors.push('Component must have at least one section: <template>, <script>, or <style>');
140
+ errors.push('Block must have at least one section: <template>, <script>, or <style>');
141
141
  }
142
142
 
143
- if (!parsed.componentName) {
144
- errors.push('Unable to derive component name from filename');
143
+ if (!parsed.blockName) {
144
+ errors.push('Unable to derive block name from filename');
145
145
  }
146
146
 
147
- if (parsed.componentName && !/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(parsed.componentName)) {
148
- errors.push(`Component name "${parsed.componentName}" must be kebab-case with at least one hyphen (e.g., "my-component")`);
147
+ if (parsed.blockName && !/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(parsed.blockName)) {
148
+ errors.push(`Block name "${parsed.blockName}" must be kebab-case with at least one hyphen (e.g., "my-block")`);
149
149
  }
150
150
 
151
151
  return {
package/vite-plugin.js CHANGED
@@ -1,25 +1,25 @@
1
1
  /**
2
- * Vite plugin for Lego Single File Components
2
+ * Vite plugin for Lego Single File Blocks
3
3
  * Auto-discovers and transforms .lego files
4
4
  */
5
5
 
6
- import { parseLego, generateDefineCall, validateLego } from './parse-lego.js';
6
+ import { parseLego, generateBlockCall, validateLego } from './parse-lego.js';
7
7
  import path from 'path';
8
8
  import fg from 'fast-glob';
9
9
 
10
- const VIRTUAL_MODULE_ID = 'virtual:lego-components';
10
+ const VIRTUAL_MODULE_ID = 'virtual:lego-blocks';
11
11
  const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
12
12
 
13
13
  /**
14
14
  * Vite plugin for Lego SFC support
15
15
  * @param {object} options - Plugin options
16
- * @param {string} options.componentsDir - Directory to search for .lego files
16
+ * @param {string} options.blocksDir - Directory to search for .lego files
17
17
  * @param {string[]} options.include - Glob patterns to include
18
18
  * @returns {import('vite').Plugin}
19
19
  */
20
20
  export default function legoPlugin(options = {}) {
21
21
  const {
22
- componentsDir = './src/components',
22
+ blocksDir = './src/blocks',
23
23
  include = ['**/*.lego'],
24
24
  importPath = 'lego-dom'
25
25
  } = options;
@@ -30,7 +30,7 @@ export default function legoPlugin(options = {}) {
30
30
 
31
31
  const getSearchPath = () => {
32
32
  const root = config?.root || process.cwd();
33
- return path.resolve(root, componentsDir);
33
+ return path.resolve(root, blocksDir);
34
34
  };
35
35
 
36
36
  const scanFiles = async () => {
@@ -75,13 +75,13 @@ export default function legoPlugin(options = {}) {
75
75
  server.watcher.add(searchPath);
76
76
  server.watcher.on('add', (file) => {
77
77
  if (file.endsWith('.lego')) {
78
- console.log(`[vite-plugin-lego] New component detected: ${path.basename(file)}`);
78
+ console.log(`[vite-plugin-lego] New block detected: ${path.basename(file)}`);
79
79
  scanFiles().then(invalidateVirtualModule);
80
80
  }
81
81
  });
82
82
  server.watcher.on('unlink', (file) => {
83
83
  if (file.endsWith('.lego')) {
84
- console.log(`[vite-plugin-lego] Component removed: ${path.basename(file)}`);
84
+ console.log(`[vite-plugin-lego] Block removed: ${path.basename(file)}`);
85
85
  scanFiles().then(invalidateVirtualModule);
86
86
  }
87
87
  });
@@ -90,7 +90,7 @@ export default function legoPlugin(options = {}) {
90
90
  async buildStart() {
91
91
  await scanFiles();
92
92
  if (legoFiles.length > 0) {
93
- console.log(`[vite-plugin-lego] Discovered ${legoFiles.length} component(s):`);
93
+ console.log(`[vite-plugin-lego] Discovered ${legoFiles.length} block(s):`);
94
94
  legoFiles.forEach(file => {
95
95
  const name = path.basename(file);
96
96
  console.log(` - ${name}`);
@@ -105,13 +105,13 @@ export default function legoPlugin(options = {}) {
105
105
  },
106
106
 
107
107
  async load(id) {
108
- // Handle virtual module that imports all .lego components
108
+ // Handle virtual module that imports all .lego blocks
109
109
  if (id === RESOLVED_VIRTUAL_MODULE_ID) {
110
110
  const imports = legoFiles.map((file, index) =>
111
- `import component${index} from '${file}?lego-component';`
111
+ `import block${index} from '${file}?lego-block';`
112
112
  ).join('\n');
113
113
 
114
- const exports = `export default function registerComponents() {\n // Components are auto-registered when imported\n}`;
114
+ const exports = `export default function registerBlocks() {\n // Blocks are auto-registered when imported\n}`;
115
115
 
116
116
  return `${imports}\n\n${exports}`;
117
117
  }
@@ -130,14 +130,14 @@ export default function legoPlugin(options = {}) {
130
130
  throw new Error(`Invalid .lego file "${filename}":\n${validation.errors.join('\n')}`);
131
131
  }
132
132
 
133
- const defineCall = generateDefineCall(parsed);
133
+ const blockCall = generateBlockCall(parsed);
134
134
 
135
135
  return `
136
136
  import { Lego } from '${importPath}';
137
137
 
138
- ${defineCall}
138
+ ${blockCall}
139
139
 
140
- export default '${parsed.componentName}';
140
+ export default '${parsed.blockName}';
141
141
  `;
142
142
  }
143
143
  },
@@ -145,7 +145,7 @@ export default '${parsed.componentName}';
145
145
  handleHotUpdate({ file, server }) {
146
146
  if (file.endsWith('.lego')) {
147
147
  console.log(`[vite-plugin-lego] Hot reload: ${path.basename(file)}`);
148
- // Trigger full reload for component content changes
148
+ // Trigger full reload for block content changes
149
149
  server.ws.send({
150
150
  type: 'full-reload',
151
151
  path: '*'
@@ -1,107 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { parseLego, generateDefineCall, validateLego, deriveComponentName } from './parse-lego.js';
3
-
4
- describe('Lego SFC Parser', () => {
5
- describe('deriveComponentName', () => {
6
- it('should derive component name from filename', () => {
7
- expect(deriveComponentName('sample-component.lego')).toBe('sample-component');
8
- expect(deriveComponentName('path/to/my-button.lego')).toBe('my-button');
9
- });
10
- });
11
-
12
- describe('parseLego', () => {
13
- it('should parse all three sections', () => {
14
- const content = `
15
- <template>
16
- <h1>{{ title }}</h1>
17
- </template>
18
-
19
- <script>
20
- export default {
21
- title: 'Hello'
22
- }
23
- </script>
24
-
25
- <style>
26
- self { color: red; }
27
- </style>
28
- `;
29
-
30
- const result = parseLego(content, 'test-component.lego');
31
- expect(result.componentName).toBe('test-component');
32
- expect(result.template).toContain('<h1>{{ title }}</h1>');
33
- expect(result.script).toContain('export default');
34
- expect(result.style).toContain('self { color: red; }');
35
- });
36
-
37
- it('should handle components with only template', () => {
38
- const content = '<template><p>Hello</p></template>';
39
- const result = parseLego(content, 'simple.lego');
40
- expect(result.template).toBe('<p>Hello</p>');
41
- expect(result.script).toBe('');
42
- expect(result.style).toBe('');
43
- });
44
-
45
- it('should handle components with only script', () => {
46
- const content = '<script>export default { count: 0 }</script>';
47
- const result = parseLego(content, 'logic.lego');
48
- expect(result.script).toContain('count: 0');
49
- expect(result.template).toBe('');
50
- });
51
- });
52
-
53
- describe('validateLego', () => {
54
- it('should validate correct component name', () => {
55
- const parsed = {
56
- componentName: 'my-component',
57
- template: '<div>Test</div>',
58
- script: '',
59
- style: ''
60
- };
61
- const result = validateLego(parsed);
62
- expect(result.valid).toBe(true);
63
- expect(result.errors).toHaveLength(0);
64
- });
65
-
66
- it('should reject invalid component names', () => {
67
- const parsed = {
68
- componentName: 'MyComponent', // Not kebab-case
69
- template: '<div>Test</div>',
70
- script: '',
71
- style: ''
72
- };
73
- const result = validateLego(parsed);
74
- expect(result.valid).toBe(false);
75
- expect(result.errors.length).toBeGreaterThan(0);
76
- });
77
-
78
- it('should require at least one section', () => {
79
- const parsed = {
80
- componentName: 'empty-component',
81
- template: '',
82
- script: '',
83
- style: ''
84
- };
85
- const result = validateLego(parsed);
86
- expect(result.valid).toBe(false);
87
- });
88
- });
89
-
90
- describe('generateDefineCall', () => {
91
- it('should generate valid Lego.define call', () => {
92
- const parsed = {
93
- componentName: 'test-comp',
94
- template: '<button>Click</button>',
95
- script: 'export default { count: 0 }',
96
- style: 'self { color: blue; }'
97
- };
98
-
99
- const result = generateDefineCall(parsed);
100
- expect(result).toContain('Lego.define');
101
- expect(result).toContain('test-comp');
102
- expect(result).toContain('<button>Click</button>');
103
- expect(result).toContain('{ count: 0 }');
104
- expect(result).toContain('self { color: blue; }');
105
- });
106
- });
107
- });