lego-dom 1.5.1 → 2.0.1

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,37 @@
1
1
  # Changelog
2
2
 
3
+ # Changelog
4
+
5
+ ## [2.0.1] - 2026-01-19
6
+
7
+ ### The "Blocks" Update Refactor 🧱
8
+
9
+ 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").
10
+
11
+ ### Breaking Changes 🚨
12
+
13
+ - **Terminological Refactor (Runtime & Docs):**
14
+ - **Components → Blocks:** The fundamental unit of UI is now a "Block".
15
+ - **SFC → Lego File:** Protocol-agnostic Single File Blocks.
16
+
17
+ - **API Renames:**
18
+ - `Lego.define()` is now **`Lego.block()`**. (Legacy alias maintained)
19
+ - `Lego.defineSFC()` is now **`Lego.defineLegoFile()`**.
20
+ - Internal: `deriveComponentName` is now `deriveBlockName`.
21
+
22
+ - **Attribute Renames:**
23
+ - `b-data` is now **`b-logic`**. (Legacy alias maintained)
24
+ - `b-styles` is now **`b-stylesheets`**.
25
+
26
+ ### Features
27
+
28
+ - **Public State API:**
29
+ - `element.state` is now the official public API for accessing the reactive proxy.
30
+ - `element._studs` is considered internal/private.
31
+
32
+ - **Lego Studio Audit:**
33
+ - `lego-studio` has been refactored to use "Block" terminology throughout the UI and codebase.
34
+
3
35
  ## [1.5.1] - 2026-01-19
4
36
 
5
37
  ### 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
  };
@@ -274,8 +274,8 @@ const Lego = (() => {
274
274
  }
275
275
  };
276
276
 
277
- const bind = (container, componentRoot, loopCtx = null) => {
278
- const state = componentRoot._studs;
277
+ const bind = (container, blockRoot, loopCtx = null) => {
278
+ const state = blockRoot._studs;
279
279
 
280
280
  const bindNode = (child) => {
281
281
  const childData = getPrivateData(child);
@@ -291,7 +291,7 @@ const Lego = (() => {
291
291
  try {
292
292
  let evalScope = state;
293
293
  if (loopCtx) {
294
- const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
294
+ const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
295
295
  const item = list[loopCtx.index];
296
296
  evalScope = Object.assign(Object.create(state), { [loopCtx.name]: item });
297
297
  }
@@ -309,7 +309,7 @@ const Lego = (() => {
309
309
  try {
310
310
  let target, last;
311
311
  if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
312
- const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
312
+ const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
313
313
  const item = list[loopCtx.index];
314
314
  if (!item) return;
315
315
  const subPath = prop.split('.').slice(1);
@@ -357,7 +357,7 @@ const Lego = (() => {
357
357
  let curr = n.parentNode;
358
358
  while (curr && curr !== container) {
359
359
  if (curr.hasAttribute && curr.hasAttribute('b-for')) return true;
360
- // Only stop at Shadow Roots or explicit boundaries, NOT component tags in Light DOM
360
+ // Only stop at Shadow Roots or explicit boundaries, NOT block tags in Light DOM
361
361
  // The parent MUST be able to bind data to the slots of its children.
362
362
  curr = curr.parentNode;
363
363
  }
@@ -462,7 +462,7 @@ const Lego = (() => {
462
462
  if (config.metrics && config.metrics.onRenderStart) config.metrics.onRenderStart(el);
463
463
 
464
464
  try {
465
- // Use shadowRoot if it's a component, otherwise render the element itself (light DOM)
465
+ // Use shadowRoot if it's a block, otherwise render the element itself (light DOM)
466
466
  const target = el.shadowRoot || el;
467
467
  if (!data.bindings) data.bindings = scanForBindings(target);
468
468
 
@@ -525,9 +525,9 @@ const Lego = (() => {
525
525
  }
526
526
  });
527
527
 
528
- // Global Broadcast: Only notify components that depend on globals
528
+ // Global Broadcast: Only notify blocks that depend on globals
529
529
  if (state === Lego.globals) {
530
- activeComponents.forEach(comp => {
530
+ activeBlocks.forEach(comp => {
531
531
  if (getPrivateData(comp).hasGlobalDependency) render(comp);
532
532
  });
533
533
  }
@@ -558,12 +558,12 @@ const Lego = (() => {
558
558
  }
559
559
  }
560
560
 
561
- // TIER 1: Logic from Lego.define (SFC)
561
+ // TIER 1: Logic from Lego.block (Lego File)
562
562
  // TIER 2: Logic from the <template b-data="..."> attribute
563
563
  // 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') || '{}');
564
+ const scriptLogic = legoFileLogic.get(name) || {};
565
+ const templateLogic = parseJSObject(templateNode.getAttribute('b-logic') || templateNode.getAttribute('b-data') || '{}');
566
+ const instanceLogic = parseJSObject(el.getAttribute('b-logic') || el.getAttribute('b-data') || '{}');
567
567
 
568
568
  el._studs = reactive({
569
569
  ...scriptLogic,
@@ -593,7 +593,7 @@ const Lego = (() => {
593
593
  }
594
594
 
595
595
  bind(shadow, el);
596
- activeComponents.add(el);
596
+ activeBlocks.add(el);
597
597
  render(el);
598
598
 
599
599
  [...shadow.children].forEach(snap);
@@ -619,7 +619,7 @@ const Lego = (() => {
619
619
  [...el.shadowRoot.children].forEach(unsnap);
620
620
  }
621
621
 
622
- activeComponents.delete(el);
622
+ activeBlocks.delete(el);
623
623
  [...el.children].forEach(unsnap);
624
624
  };
625
625
 
@@ -701,7 +701,7 @@ const Lego = (() => {
701
701
  snap(n);
702
702
  // Auto-Discovery: Check if tag is unknown and loader is configured
703
703
  const tagName = n.tagName.toLowerCase();
704
- if (tagName.includes('-') && !registry[tagName] && config.loader && !activeComponents.has(n)) {
704
+ if (tagName.includes('-') && !registry[tagName] && config.loader && !activeBlocks.has(n)) {
705
705
  const result = config.loader(tagName);
706
706
  if (result) {
707
707
  // Handle Promise (user does custom fetch) vs String (we fetch)
@@ -710,7 +710,7 @@ const Lego = (() => {
710
710
  : result;
711
711
 
712
712
  Promise.resolve(promise)
713
- .then(sfc => publicAPI.defineSFC(sfc, tagName + '.lego'))
713
+ .then(legoFile => publicAPI.defineLegoFile(legoFile, tagName + '.lego'))
714
714
  .catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
715
715
  }
716
716
  }
@@ -774,7 +774,7 @@ const Lego = (() => {
774
774
  },
775
775
  $go: (path, ...targets) => _go(path, ...targets)(document.body)
776
776
  }, document.body),
777
- defineSFC: (content, filename = 'component.lego') => {
777
+ defineLegoFile: (content, filename = 'block.lego') => {
778
778
  let template = '';
779
779
  let script = '{}';
780
780
  let stylesAttr = '';
@@ -820,7 +820,7 @@ const Lego = (() => {
820
820
  remaining = remaining.slice(contentEnd + closeTag.length);
821
821
  }
822
822
 
823
- const name = deriveComponentName(filename);
823
+ const name = deriveBlockName(filename);
824
824
  // We must eval the script to get the object.
825
825
  // Safe-ish because it's coming from the "Server" (trusted source in this architecture)
826
826
  const logicObj = new Function(`return ${script}`)();
@@ -832,18 +832,18 @@ const Lego = (() => {
832
832
  registry[name] = document.createElement('template');
833
833
  registry[name].innerHTML = template;
834
834
  registry[name].setAttribute('b-stylesheets', stylesAttr);
835
- sfcLogic.set(name, logicObj);
835
+ legoFileLogic.set(name, logicObj);
836
836
 
837
837
  // Upgrade existing elements
838
838
  document.querySelectorAll(name).forEach(el => !getPrivateData(el).snapped && snap(el));
839
839
  },
840
- define: (tagName, templateHTML, logic = {}, styles = "") => {
840
+ block: (tagName, templateHTML, logic = {}, styles = "") => {
841
841
  const t = document.createElement('template');
842
842
  t.setAttribute('b-id', tagName);
843
843
  t.setAttribute('b-stylesheets', styles);
844
844
  t.innerHTML = templateHTML;
845
845
  registry[tagName] = t;
846
- sfcLogic.set(tagName, logic);
846
+ legoFileLogic.set(tagName, logic);
847
847
 
848
848
  // Initialize shared state with try-catch safety
849
849
  try {
@@ -854,8 +854,11 @@ const Lego = (() => {
854
854
 
855
855
  document.querySelectorAll(tagName).forEach(snap);
856
856
  },
857
+ // Alias for backward compatibility
858
+ get define() { return this.block },
859
+
857
860
  // For specific test validation
858
- getActiveComponentsCount: () => activeComponents.size,
861
+ getActiveBlocksCount: () => activeBlocks.size,
859
862
  getLegos: () => Object.keys(registry),
860
863
  config, // Expose config for customization
861
864
  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.1",
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
- });