lucent-ui 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -74,7 +74,7 @@
74
74
  .lucent-table-striped tbody .lucent-table-row:nth-child(even) > th {
75
75
  background: color-mix(in srgb, var(--lucent-text-primary) 4%, transparent);
76
76
  }
77
- `;function In({children:t,style:r,...n}){return e.jsx("thead",{style:{background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",...r},...n,children:t})}function jn({children:t,...r}){return e.jsx("tbody",{...r,children:t})}function Mn({children:t,style:r,...n}){return e.jsx("tfoot",{style:{background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",...r},...n,children:t})}function zn({children:t,className:r,...n}){return e.jsx("tr",{className:["lucent-table-row",r].filter(Boolean).join(" "),...n,children:t})}function En({as:t,children:r,style:n,...o}){const a=t==="th",i={padding:"var(--lucent-space-3) var(--lucent-space-4)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-sm)",borderBottom:"1px solid var(--lucent-border-default)",textAlign:"left",verticalAlign:"middle",color:a?"var(--lucent-text-secondary)":"var(--lucent-text-primary)",fontWeight:a?"var(--lucent-font-weight-semibold)":"var(--lucent-font-weight-regular)",whiteSpace:a?"nowrap":void 0,...n};return a?e.jsx("th",{scope:"col",style:i,...o,children:r}):e.jsx("td",{style:i,...o,children:r})}function ne({striped:t=!1,children:r,className:n,style:o,...a}){const i=["lucent-table",t&&"lucent-table-striped",n].filter(Boolean).join(" ");return e.jsxs(e.Fragment,{children:[e.jsx("style",{children:Cn}),e.jsx("div",{style:{overflowX:"auto",width:"100%"},children:e.jsx("table",{className:i,style:{width:"100%",borderCollapse:"collapse",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-sm)",...o},...a,children:r})})]})}ne.Head=In;ne.Body=jn;ne.Foot=Mn;ne.Row=zn;ne.Cell=En;const An={id:"table",name:"Table",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A lightweight, token-styled HTML table primitive with compound sub-components. Distinct from DataTable — no sorting, filtering, or pagination.",designIntent:"Use Table for static or lightly dynamic tabular data where full DataTable features are not needed — props tables, changelog entries, comparison grids, reference docs. The compound API (Table.Head, Table.Body, Table.Row, Table.Cell) maps directly to semantic HTML so screen readers get the full table structure. Horizontal overflow is handled automatically by a scroll wrapper.",props:[{name:"striped",type:"boolean",required:!1,default:"false",description:"Applies alternating tinted backgrounds to even tbody rows via color-mix(transparent)."},{name:"Table.Head",type:"component",required:!1,description:"Renders <thead> with a subtle tinted background. Accepts Table.Row children."},{name:"Table.Body",type:"component",required:!1,description:"Renders <tbody>. Accepts Table.Row children."},{name:"Table.Foot",type:"component",required:!1,description:"Renders <tfoot> with a subtle tinted background."},{name:"Table.Row",type:"component",required:!1,description:"Renders <tr> with a hover highlight. Accepts Table.Cell children."},{name:"Table.Cell",type:"component",required:!1,description:'Renders <td> by default or <th scope="col"> when as="th". Header cells are semibold + secondary colour; data cells are regular + primary.'}],usageExamples:[{title:"Basic",code:`<Table>
77
+ `;function In({children:t,style:r,...n}){return e.jsx("thead",{style:{background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",...r},...n,children:t})}function jn({children:t,...r}){return e.jsx("tbody",{...r,children:t})}function Mn({children:t,style:r,...n}){return e.jsx("tfoot",{style:{background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",...r},...n,children:t})}function zn({children:t,className:r,...n}){return e.jsx("tr",{className:["lucent-table-row",r].filter(Boolean).join(" "),...n,children:t})}function En({as:t,children:r,style:n,...o}){const a=t==="th",i={padding:"var(--lucent-space-3) var(--lucent-space-4)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-sm)",borderBottom:"1px solid var(--lucent-border-default)",textAlign:"left",verticalAlign:"middle",color:a?"var(--lucent-text-secondary)":"var(--lucent-text-primary)",fontWeight:a?"var(--lucent-font-weight-semibold)":"var(--lucent-font-weight-regular)",whiteSpace:a?"nowrap":void 0,...n};return a?e.jsx("th",{scope:"col",style:i,...o,children:r}):e.jsx("td",{style:i,...o,children:r})}function ne({striped:t=!1,children:r,className:n,style:o,...a}){const i=["lucent-table",t&&"lucent-table-striped",n].filter(Boolean).join(" ");return e.jsxs(e.Fragment,{children:[e.jsx("style",{children:Cn}),e.jsx("div",{style:{overflowX:"auto",width:"100%",background:"var(--lucent-surface)"},children:e.jsx("table",{className:i,style:{width:"100%",borderCollapse:"collapse",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-sm)",...o},...a,children:r})})]})}ne.Head=In;ne.Body=jn;ne.Foot=Mn;ne.Row=zn;ne.Cell=En;const An={id:"table",name:"Table",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A lightweight, token-styled HTML table primitive with compound sub-components. Distinct from DataTable — no sorting, filtering, or pagination.",designIntent:"Use Table for static or lightly dynamic tabular data where full DataTable features are not needed — props tables, changelog entries, comparison grids, reference docs. The compound API (Table.Head, Table.Body, Table.Row, Table.Cell) maps directly to semantic HTML so screen readers get the full table structure. Horizontal overflow is handled automatically by a scroll wrapper. The wrapper paints var(--lucent-surface) as its background so the table always sits on a solid panel, regardless of parent page color; thead/tfoot/striped tints are translucent color-mix overlays on top of that surface so they adapt to both light and dark modes.",props:[{name:"striped",type:"boolean",required:!1,default:"false",description:"Applies alternating tinted backgrounds to even tbody rows via color-mix(transparent)."},{name:"Table.Head",type:"component",required:!1,description:"Renders <thead> with a subtle tinted background. Accepts Table.Row children."},{name:"Table.Body",type:"component",required:!1,description:"Renders <tbody>. Accepts Table.Row children."},{name:"Table.Foot",type:"component",required:!1,description:"Renders <tfoot> with a subtle tinted background."},{name:"Table.Row",type:"component",required:!1,description:"Renders <tr> with a hover highlight. Accepts Table.Cell children."},{name:"Table.Cell",type:"component",required:!1,description:'Renders <td> by default or <th scope="col"> when as="th". Header cells are semibold + secondary colour; data cells are regular + primary.'}],usageExamples:[{title:"Basic",code:`<Table>
78
78
  <Table.Head>
79
79
  <Table.Row>
80
80
  <Table.Cell as="th">Name</Table.Cell>
@@ -339,7 +339,7 @@ const [results, setResults] = useState([]);
339
339
  [data-lucent-collapsible-trigger]:focus-visible {
340
340
  box-shadow: 0 0 0 2px var(--lucent-surface), 0 0 0 4px var(--lucent-accent-default) !important;
341
341
  }
342
- `;let Ke=!1;function _r(){if(Ke||typeof document>"u")return;const t=document.createElement("style");t.setAttribute("data-lucent-collapsible",""),t.textContent=Ur,document.head.appendChild(t),Ke=!0}function Yr({trigger:t,children:r,defaultOpen:n=!1,open:o,onOpenChange:a,padded:i=!0,disabled:l=!1,style:s}){const d=c.useContext(ye),p=d.px!=="0"||d.py!=="0",h=o!==void 0,[f,w]=c.useState(n),y=h?o:f,g=c.useRef(null),m=c.useRef(!1),u=c.useRef();c.useEffect(_r,[]),c.useEffect(()=>{m.current=!0},[]),c.useLayoutEffect(()=>{const x=g.current;if(x)if(y){x.style.overflow="hidden";const k=x.scrollHeight;x.style.height=`${k}px`,x.style.transition=Ye,clearTimeout(u.current),u.current=setTimeout(()=>{x.style.height="auto",x.style.overflow="visible",x.style.transition=""},mt+20)}else m.current&&(clearTimeout(u.current),x.style.overflow="hidden",x.style.transition="",x.style.height=`${x.scrollHeight}px`,x.getBoundingClientRect(),x.style.transition=Ye,x.style.height="0px")},[y]),c.useEffect(()=>{const x=g.current;x&&!y&&!m.current&&(x.style.height="0px")},[]),c.useEffect(()=>()=>clearTimeout(u.current),[]);const b=()=>{if(l)return;const x=!y;h||w(x),a==null||a(x)};return e.jsxs("div",{style:{display:"flex",flexDirection:"column",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-md)",...p&&{margin:`calc(-1 * ${d.py}) calc(-1 * ${d.px})`},...s},children:[e.jsxs("button",{"data-lucent-collapsible-trigger":!0,onClick:b,disabled:l,"aria-expanded":y,style:{display:"flex",alignItems:"center",justifyContent:"space-between",width:"100%",background:"none",border:"none",borderRadius:"var(--lucent-radius-md)",padding:"var(--lucent-space-4)",cursor:l?"not-allowed":"pointer",textAlign:"left",outline:"none",color:"inherit",fontFamily:"inherit",fontSize:"inherit",opacity:l?.5:1,transition:`background ${Ae} ${te}`},children:[e.jsx("span",{style:{flex:1},children:t}),e.jsx(Kr,{open:y})]}),e.jsx("div",{ref:g,"aria-hidden":!y,style:{overflow:y?"visible":"hidden"},children:e.jsx("div",{style:{...i?{padding:"var(--lucent-space-3) var(--lucent-space-4) var(--lucent-space-4)"}:{},opacity:y?1:0,transform:y?"translateY(0)":"translateY(-4px)",transition:`opacity ${_e}ms ${te}, transform ${_e}ms ${te}`},children:r})})]})}function Kr({open:t}){return e.jsx("svg",{"data-lucent-collapsible-chevron":!0,width:16,height:16,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":!0,style:{flexShrink:0,color:"var(--lucent-text-secondary)",transform:t?"rotate(180deg)":"rotate(0deg)",transition:`transform ${Ae} ${te}, color ${Ae} ${te}`},children:e.jsx("polyline",{points:"6 9 12 15 18 9"})})}const oe="lucent-pl-no-scrollbar",Xr=`.${oe}{scrollbar-width:none}.${oe}::-webkit-scrollbar{display:none}`;function pe(t){return typeof t=="number"?`${t}px`:t}function Jr({children:t,header:r,sidebar:n,sidebarWidth:o=240,headerHeight:a=48,sidebarCollapsed:i=!1,rightSidebar:l,rightSidebarWidth:s=240,rightSidebarCollapsed:d=!1,footer:p,footerHeight:h=28,chromeBackground:f="navigation",mainStyle:w,style:y}){const g=pe(a),m=pe(o),u=pe(s),b=pe(h),k={navigation:"var(--lucent-navigation)",bgBase:"var(--lucent-bg-base)",bgSubtle:"var(--lucent-bg-subtle)",surface:"var(--lucent-surface)",surfaceSecondary:"var(--lucent-surface-secondary)"}[f]??"var(--lucent-navigation)";return e.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100vh",overflow:"hidden",background:k,fontFamily:"var(--lucent-font-family-base)",...y},children:[e.jsx("style",{children:Xr}),r!=null&&e.jsx("div",{style:{flexShrink:0,height:g,zIndex:10,background:k},children:r}),e.jsxs("div",{style:{display:"flex",flex:1,overflow:"hidden"},children:[n!=null&&e.jsx("div",{className:oe,style:{width:i?0:m,flexShrink:0,overflow:"hidden",overflowY:i?"hidden":"auto",background:k,transition:"width 200ms var(--lucent-easing-default)"},children:n}),e.jsx("main",{className:oe,style:{flex:1,overflowY:"auto",minWidth:0,margin:l!=null?"0 0 var(--lucent-space-3) 0":"0 var(--lucent-space-3) var(--lucent-space-3) 0",border:"1px solid var(--lucent-border-default)",borderRadius:"var(--lucent-radius-lg)",boxShadow:"var(--lucent-shadow-sm)",background:"var(--lucent-bg-base)",...w},children:t}),l!=null&&e.jsx("aside",{className:oe,style:{width:d?0:u,flexShrink:0,overflow:"hidden",overflowY:d?"hidden":"auto",background:k,transition:"width 200ms var(--lucent-easing-default)"},children:l})]}),p!=null&&e.jsx("div",{style:{flexShrink:0,height:b,zIndex:10,background:k},children:p})]})}function Zr({state:t}){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 12 12",fill:"none","aria-hidden":!0,style:{flexShrink:0,opacity:t==="none"?.35:1},children:[e.jsx("path",{d:"M6 2L9 5H3L6 2Z",fill:"currentColor",opacity:t==="desc"?.35:1}),e.jsx("path",{d:"M6 10L3 7H9L6 10Z",fill:"currentColor",opacity:t==="asc"?.35:1})]})}function Xe({dir:t}){return e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":!0,children:e.jsx("path",{d:t==="left"?"M10 12L6 8l4-4":"M6 4l4 4-4 4",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})}function Qr({columns:t,rows:r,pageSize:n=10,page:o,onPageChange:a,onFilterChange:i,emptyState:l,style:s}){const[d,p]=c.useState(null),[h,f]=c.useState(0),[w,y]=c.useState(null),[g,m]=c.useState({}),u=o!==void 0,b=u?o:h,x=t.some(j=>j.filterable),k=x?r.filter(j=>t.every(P=>{if(!P.filterable)return!0;const D=g[P.key];if(!D||D.length===0)return!0;const W=String(j[P.key]??"");return D.includes(W)})):r,S=d?[...k].sort((j,P)=>{const D=j[d.key],W=P[d.key],q=String(D??"").localeCompare(String(W??""),void 0,{numeric:!0});return d.dir==="asc"?q:-q}):k,C=n>0?S.slice(b*n,(b+1)*n):S,v=n>0?Math.max(1,Math.ceil(S.length/n)):1,z=j=>{u||f(j),a==null||a(j)},T=j=>{p(P=>!P||P.key!==j?{key:j,dir:"asc"}:P.dir==="asc"?{key:j,dir:"desc"}:null),u||f(0),a==null||a(0)},E=(j,P)=>{const D={...g,[j]:P};P.length===0&&delete D[j],m(D),u||f(0),a==null||a(0),i==null||i(D)},F=()=>{m({}),u||f(0),a==null||a(0),i==null||i({})},R=[];if(v<=7)for(let j=0;j<v;j++)R.push(j);else{R.push(0),b>2&&R.push("…");for(let j=Math.max(1,b-1);j<=Math.min(v-2,b+1);j++)R.push(j);b<v-3&&R.push("…"),R.push(v-1)}return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-3)",...s},children:[x&&e.jsx("div",{style:{position:"relative",zIndex:1},children:e.jsx(ea,{columns:t,rows:r,filters:g,onFilter:E,onClearAll:F})}),e.jsx("div",{style:{overflowX:"auto",borderRadius:"var(--lucent-radius-lg)",border:"1px solid var(--lucent-border-default)"},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-sm)"},children:[e.jsx("thead",{children:e.jsx("tr",{style:{borderBottom:"1px solid var(--lucent-border-default)"},children:t.map(j=>{const P=(d==null?void 0:d.key)===j.key?d.dir:"none";return e.jsx("th",{onClick:j.sortable?()=>T(j.key):void 0,style:{padding:"var(--lucent-space-3) var(--lucent-space-4)",textAlign:j.align??"left",fontWeight:"var(--lucent-font-weight-medium)",color:"var(--lucent-text-secondary)",background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",borderBottom:"1px solid var(--lucent-border-default)",cursor:j.sortable?"pointer":"default",userSelect:"none",whiteSpace:"nowrap",...j.width?{width:j.width}:{}},children:e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-1)"},children:[j.header,j.sortable&&e.jsx(Zr,{state:P}),j.headerFilter&&e.jsx("span",{onClick:D=>D.stopPropagation(),style:{display:"inline-flex",marginLeft:"var(--lucent-space-1)"},children:j.headerFilter})]})},j.key)})})}),e.jsx("tbody",{children:C.length===0?e.jsx("tr",{children:e.jsx("td",{colSpan:t.length,style:{padding:"var(--lucent-space-12)",textAlign:"center"},children:l??e.jsx(I.Text,{color:"secondary",children:"No data"})})}):C.map((j,P)=>e.jsx("tr",{onMouseEnter:()=>y(P),onMouseLeave:()=>y(null),style:{borderBottom:P<C.length-1?"1px solid var(--lucent-border-subtle)":"none",background:w===P?"color-mix(in srgb, var(--lucent-text-primary) 4%, transparent)":"transparent",transition:"background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:t.map(D=>e.jsx("td",{style:{padding:"var(--lucent-space-3) var(--lucent-space-4)",color:"var(--lucent-text-primary)",textAlign:D.align??"left",verticalAlign:"middle"},children:D.render?D.render(j,P):String(j[D.key]??"")},D.key))},P))})]})}),n>0&&S.length>0&&e.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"var(--lucent-space-3)",flexWrap:"wrap"},children:[e.jsx(I.Text,{color:"secondary",size:"sm",children:S.length===0?`0 rows${r.length>0?` (filtered from ${r.length})`:""}`:S.length===1?`1 row${S.length<r.length?` (filtered from ${r.length})`:""}`:`${b*n+1}–${Math.min((b+1)*n,S.length)} of ${S.length} rows${S.length<r.length?` (filtered from ${r.length})`:""}`}),e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-1)"},children:[e.jsx(ke,{onClick:()=>z(b-1),disabled:b===0,"aria-label":"Previous page",children:e.jsx(Xe,{dir:"left"})}),R.map((j,P)=>j==="…"?e.jsx("span",{style:{padding:"0 var(--lucent-space-1)",color:"var(--lucent-text-disabled)",fontSize:"var(--lucent-font-size-sm)"},children:"…"},`ellipsis-${P}`):e.jsx(ke,{onClick:()=>z(j),active:j===b,"aria-label":`Page ${j+1}`,"aria-current":j===b?"page":void 0,children:j+1},j)),e.jsx(ke,{onClick:()=>z(b+1),disabled:b>=v-1,"aria-label":"Next page",children:e.jsx(Xe,{dir:"right"})})]})]})]})}function ea({columns:t,rows:r,filters:n,onFilter:o,onClearAll:a}){const i=t.filter(s=>s.filterable),l=Object.keys(n).length;return e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-2)",flexWrap:"wrap"},children:[i.map(s=>{const d=Array.from(new Set(r.map(p=>String(p[s.key]??"")))).sort();return e.jsx(ta,{label:s.header,values:d,value:n[s.key]??[],onChange:p=>o(s.key,p)},s.key)}),l>0&&e.jsx("button",{onClick:a,style:{display:"inline-flex",alignItems:"center",height:30,padding:"0 var(--lucent-space-2)",border:"none",borderRadius:"var(--lucent-radius-md)",background:"transparent",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",cursor:"pointer"},children:"Clear all"})]})}function ta({label:t,values:r,value:n,onChange:o}){const[a,i]=c.useState(!1),[l,s]=c.useState(!1),[d,p]=c.useState(""),h=c.useRef(null),f=c.useRef(null),w=n.length>0;c.useEffect(()=>{if(!a){p("");return}setTimeout(()=>{var x;return(x=f.current)==null?void 0:x.focus()},0);const u=x=>{h.current&&!h.current.contains(x.target)&&i(!1)},b=x=>{x.key==="Escape"&&i(!1)};return document.addEventListener("mousedown",u),document.addEventListener("keydown",b),()=>{document.removeEventListener("mousedown",u),document.removeEventListener("keydown",b)}},[a]);const y=d?r.filter(u=>u.toLowerCase().includes(d.toLowerCase())):r,g=u=>o(n.includes(u)?n.filter(b=>b!==u):[...n,u]),m=n.length===0?null:n.length===1?e.jsxs("span",{style:{color:"var(--lucent-text-secondary)",fontWeight:"var(--lucent-font-weight-regular)"},children:[": ",n[0]]}):e.jsxs("span",{style:{color:"var(--lucent-accent-default)"},children:["(",n.length,")"]});return e.jsxs("div",{ref:h,style:{position:"relative"},children:[e.jsxs("button",{onClick:()=>i(u=>!u),onMouseEnter:()=>s(!0),onMouseLeave:()=>s(!1),style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-1)",height:30,padding:"0 var(--lucent-space-3)",borderRadius:"var(--lucent-radius-md)",border:`1px solid ${w?"var(--lucent-accent-default)":l?"var(--lucent-border-strong)":"var(--lucent-border-default)"}`,background:w?"var(--lucent-accent-subtle)":"var(--lucent-surface)",color:w?"var(--lucent-accent-default)":"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",fontWeight:w?"var(--lucent-font-weight-medium)":"var(--lucent-font-weight-regular)",cursor:"pointer",outline:"none",whiteSpace:"nowrap",transition:"border-color var(--lucent-duration-fast) var(--lucent-easing-default), background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:[t,m,e.jsx("svg",{width:"10",height:"10",viewBox:"0 0 10 10",fill:"none","aria-hidden":!0,style:{transform:a?"rotate(180deg)":"none",transition:"transform var(--lucent-duration-fast) var(--lucent-easing-default)"},children:e.jsx("path",{d:"M2 3.5L5 6.5L8 3.5",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})]}),a&&e.jsxs("div",{style:{position:"absolute",top:"calc(100% + 4px)",left:0,minWidth:180,maxHeight:280,display:"flex",flexDirection:"column",background:"var(--lucent-surface)",border:"1px solid var(--lucent-border-default)",borderRadius:"var(--lucent-radius-lg)",boxShadow:"0 4px 16px color-mix(in srgb, var(--lucent-text-primary) 8%, transparent)",zIndex:50},children:[e.jsx("div",{style:{padding:"var(--lucent-space-2)",paddingBottom:0},children:e.jsx("input",{ref:f,type:"text",value:d,onChange:u=>p(u.target.value),placeholder:"Search…",style:{width:"100%",boxSizing:"border-box",height:26,padding:"0 var(--lucent-space-2)",borderRadius:"var(--lucent-radius-md)",border:"1px solid var(--lucent-border-default)",background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",outline:"none"}})}),n.length>0&&e.jsx("div",{style:{padding:"var(--lucent-space-1) var(--lucent-space-2) 0"},children:e.jsx("button",{onClick:()=>o([]),style:{display:"inline-flex",padding:"2px var(--lucent-space-2)",border:"none",borderRadius:"var(--lucent-radius-sm)",background:"transparent",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",cursor:"pointer",textDecoration:"underline"},children:"Clear selection"})}),e.jsx("div",{style:{overflowY:"auto",padding:"var(--lucent-space-1)",borderTop:"1px solid var(--lucent-border-subtle)",marginTop:"var(--lucent-space-2)"},children:y.length===0?e.jsx("div",{style:{padding:"var(--lucent-space-3)",color:"var(--lucent-text-disabled)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",textAlign:"center"},children:"No results"}):y.map(u=>e.jsx(na,{label:u,isSelected:n.includes(u),onClick:()=>g(u)},u))})]})]})}function na({label:t,isSelected:r,onClick:n}){const[o,a]=c.useState(!1);return e.jsxs("button",{onClick:n,onMouseEnter:()=>a(!0),onMouseLeave:()=>a(!1),style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-2)",width:"100%",textAlign:"left",padding:"var(--lucent-space-2) var(--lucent-space-3)",borderRadius:"var(--lucent-radius-md)",border:"none",background:o?"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)":"transparent",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",fontWeight:r?"var(--lucent-font-weight-medium)":"var(--lucent-font-weight-regular)",cursor:"pointer",outline:"none",whiteSpace:"nowrap"},children:[e.jsx("span",{style:{flexShrink:0,width:14,height:14,borderRadius:"var(--lucent-radius-sm)",border:`1.5px solid ${r?"var(--lucent-accent-default)":"var(--lucent-border-strong)"}`,background:r?"var(--lucent-accent-default)":"transparent",display:"flex",alignItems:"center",justifyContent:"center",transition:"border-color var(--lucent-duration-fast), background var(--lucent-duration-fast)"},children:r&&e.jsx("svg",{width:"8",height:"8",viewBox:"0 0 8 8",fill:"none","aria-hidden":!0,children:e.jsx("path",{d:"M1 4L3 6L7 2",stroke:"var(--lucent-accent-fg)",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})}),t]})}function ke({children:t,onClick:r,disabled:n,active:o,...a}){const[i,l]=c.useState(!1);return e.jsx("button",{...a,onClick:r,disabled:n,onMouseEnter:()=>l(!0),onMouseLeave:()=>l(!1),style:{display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:32,height:32,padding:"0 var(--lucent-space-2)",borderRadius:"var(--lucent-radius-md)",border:o?"1px solid var(--lucent-accent-default)":"1px solid transparent",background:o?"var(--lucent-accent-default)":i&&!n?"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)":"transparent",color:o?"var(--lucent-accent-fg)":n?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",fontSize:"var(--lucent-font-size-sm)",fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-regular)",cursor:n?"not-allowed":"pointer",transition:"background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:t})}const ra={id:"data-table",name:"DataTable",tier:"molecule",domain:"neutral",specVersion:"1.0",description:"A sortable, filterable, paginated data table with configurable columns, custom cell renderers, and keyboard-accessible pagination controls.",designIntent:'DataTable is generic over row type T so TypeScript consumers get full type safety on column keys and renderers. Sorting is client-side and composable — each column opts in via sortable:true; clicking a sorted column cycles asc → desc → unsorted. Filtering is per-column — each column opts in via filterable:true, which adds a dropdown button above the table. Each dropdown is searchable and multi-select: a search input filters the option list, and each option is a checkbox that toggles membership in the active set. Filtering uses set-membership: a row passes if its column value is included in the selected values array. A "Clear selection" link inside each dropdown clears that column; a "Clear all" button in the bar appears when any filter is active. Filter → sort → paginate is the fixed pipeline order; any filter change resets the page to 0. Pagination is either controlled (page prop + onPageChange) or uncontrolled (internal state). A pageSize of 0 disables pagination entirely, useful when the parent manages windowing. Row hover uses bg-subtle, not a border change, so the visual weight stays low for dense data views.',props:[{name:"columns",type:"array",required:!0,description:"Column definitions. Each column has a key, header, optional render function, optional sortable flag, optional filterable flag (renders a text filter input below the header), optional width, and optional text align."},{name:"rows",type:"array",required:!0,description:"Array of data objects to display. The generic type T is inferred from this prop."},{name:"pageSize",type:"number",required:!1,default:"10",description:"Number of rows per page. Set to 0 to disable pagination."},{name:"page",type:"number",required:!1,description:"Controlled current page (0-indexed). When provided, the component is fully controlled."},{name:"onPageChange",type:"function",required:!1,description:"Called with the new page index whenever the page changes (from pagination controls or after a sort/filter reset)."},{name:"onFilterChange",type:"function",required:!1,description:"Called with the current filter map (Record<string, string[]>) whenever any column filter changes. Keys are column keys; columns with no selection are omitted from the map."},{name:"emptyState",type:"ReactNode",required:!1,description:'Content to render when rows is empty. Defaults to a "No data" text.'},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Basic sortable table",code:`<DataTable
342
+ `;let Ke=!1;function _r(){if(Ke||typeof document>"u")return;const t=document.createElement("style");t.setAttribute("data-lucent-collapsible",""),t.textContent=Ur,document.head.appendChild(t),Ke=!0}function Yr({trigger:t,children:r,defaultOpen:n=!1,open:o,onOpenChange:a,padded:i=!0,disabled:l=!1,style:s}){const d=c.useContext(ye),p=d.px!=="0"||d.py!=="0",h=o!==void 0,[f,w]=c.useState(n),y=h?o:f,g=c.useRef(null),m=c.useRef(!1),u=c.useRef();c.useEffect(_r,[]),c.useEffect(()=>{m.current=!0},[]),c.useLayoutEffect(()=>{const x=g.current;if(x)if(y){x.style.overflow="hidden";const k=x.scrollHeight;x.style.height=`${k}px`,x.style.transition=Ye,clearTimeout(u.current),u.current=setTimeout(()=>{x.style.height="auto",x.style.overflow="visible",x.style.transition=""},mt+20)}else m.current&&(clearTimeout(u.current),x.style.overflow="hidden",x.style.transition="",x.style.height=`${x.scrollHeight}px`,x.getBoundingClientRect(),x.style.transition=Ye,x.style.height="0px")},[y]),c.useEffect(()=>{const x=g.current;x&&!y&&!m.current&&(x.style.height="0px")},[]),c.useEffect(()=>()=>clearTimeout(u.current),[]);const b=()=>{if(l)return;const x=!y;h||w(x),a==null||a(x)};return e.jsxs("div",{style:{display:"flex",flexDirection:"column",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-md)",...p&&{margin:`calc(-1 * ${d.py}) calc(-1 * ${d.px})`},...s},children:[e.jsxs("button",{"data-lucent-collapsible-trigger":!0,onClick:b,disabled:l,"aria-expanded":y,style:{display:"flex",alignItems:"center",justifyContent:"space-between",width:"100%",background:"none",border:"none",borderRadius:"var(--lucent-radius-md)",padding:"var(--lucent-space-4)",cursor:l?"not-allowed":"pointer",textAlign:"left",outline:"none",color:"inherit",fontFamily:"inherit",fontSize:"inherit",opacity:l?.5:1,transition:`background ${Ae} ${te}`},children:[e.jsx("span",{style:{flex:1},children:t}),e.jsx(Kr,{open:y})]}),e.jsx("div",{ref:g,"aria-hidden":!y,style:{overflow:y?"visible":"hidden"},children:e.jsx("div",{style:{...i?{padding:"var(--lucent-space-3) var(--lucent-space-4) var(--lucent-space-4)"}:{},opacity:y?1:0,transform:y?"translateY(0)":"translateY(-4px)",transition:`opacity ${_e}ms ${te}, transform ${_e}ms ${te}`},children:r})})]})}function Kr({open:t}){return e.jsx("svg",{"data-lucent-collapsible-chevron":!0,width:16,height:16,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":!0,style:{flexShrink:0,color:"var(--lucent-text-secondary)",transform:t?"rotate(180deg)":"rotate(0deg)",transition:`transform ${Ae} ${te}, color ${Ae} ${te}`},children:e.jsx("polyline",{points:"6 9 12 15 18 9"})})}const oe="lucent-pl-no-scrollbar",Xr=`.${oe}{scrollbar-width:none}.${oe}::-webkit-scrollbar{display:none}`;function pe(t){return typeof t=="number"?`${t}px`:t}function Jr({children:t,header:r,sidebar:n,sidebarWidth:o=240,headerHeight:a=48,sidebarCollapsed:i=!1,rightSidebar:l,rightSidebarWidth:s=240,rightSidebarCollapsed:d=!1,footer:p,footerHeight:h=28,chromeBackground:f="navigation",mainStyle:w,style:y}){const g=pe(a),m=pe(o),u=pe(s),b=pe(h),k={navigation:"var(--lucent-navigation)",bgBase:"var(--lucent-bg-base)",bgSubtle:"var(--lucent-bg-subtle)",surface:"var(--lucent-surface)",surfaceSecondary:"var(--lucent-surface-secondary)"}[f]??"var(--lucent-navigation)";return e.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100vh",overflow:"hidden",background:k,fontFamily:"var(--lucent-font-family-base)",...y},children:[e.jsx("style",{children:Xr}),r!=null&&e.jsx("div",{style:{flexShrink:0,height:g,zIndex:10,background:k},children:r}),e.jsxs("div",{style:{display:"flex",flex:1,overflow:"hidden"},children:[n!=null&&e.jsx("div",{className:oe,style:{width:i?0:m,flexShrink:0,overflow:"hidden",overflowY:i?"hidden":"auto",background:k,transition:"width 200ms var(--lucent-easing-default)"},children:n}),e.jsx("main",{className:oe,style:{flex:1,overflowY:"auto",minWidth:0,margin:l!=null?"0 0 var(--lucent-space-3) 0":"0 var(--lucent-space-3) var(--lucent-space-3) 0",border:"1px solid var(--lucent-border-default)",borderRadius:"var(--lucent-radius-lg)",boxShadow:"var(--lucent-shadow-sm)",background:"var(--lucent-bg-base)",...w},children:t}),l!=null&&e.jsx("aside",{className:oe,style:{width:d?0:u,flexShrink:0,overflow:"hidden",overflowY:d?"hidden":"auto",background:k,transition:"width 200ms var(--lucent-easing-default)"},children:l})]}),p!=null&&e.jsx("div",{style:{flexShrink:0,height:b,zIndex:10,background:k},children:p})]})}function Zr({state:t}){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 12 12",fill:"none","aria-hidden":!0,style:{flexShrink:0,opacity:t==="none"?.35:1},children:[e.jsx("path",{d:"M6 2L9 5H3L6 2Z",fill:"currentColor",opacity:t==="desc"?.35:1}),e.jsx("path",{d:"M6 10L3 7H9L6 10Z",fill:"currentColor",opacity:t==="asc"?.35:1})]})}function Xe({dir:t}){return e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":!0,children:e.jsx("path",{d:t==="left"?"M10 12L6 8l4-4":"M6 4l4 4-4 4",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})}function Qr({columns:t,rows:r,pageSize:n=10,page:o,onPageChange:a,onFilterChange:i,emptyState:l,style:s}){const[d,p]=c.useState(null),[h,f]=c.useState(0),[w,y]=c.useState(null),[g,m]=c.useState({}),u=o!==void 0,b=u?o:h,x=t.some(j=>j.filterable),k=x?r.filter(j=>t.every(P=>{if(!P.filterable)return!0;const D=g[P.key];if(!D||D.length===0)return!0;const W=String(j[P.key]??"");return D.includes(W)})):r,S=d?[...k].sort((j,P)=>{const D=j[d.key],W=P[d.key],q=String(D??"").localeCompare(String(W??""),void 0,{numeric:!0});return d.dir==="asc"?q:-q}):k,C=n>0?S.slice(b*n,(b+1)*n):S,v=n>0?Math.max(1,Math.ceil(S.length/n)):1,z=j=>{u||f(j),a==null||a(j)},T=j=>{p(P=>!P||P.key!==j?{key:j,dir:"asc"}:P.dir==="asc"?{key:j,dir:"desc"}:null),u||f(0),a==null||a(0)},E=(j,P)=>{const D={...g,[j]:P};P.length===0&&delete D[j],m(D),u||f(0),a==null||a(0),i==null||i(D)},F=()=>{m({}),u||f(0),a==null||a(0),i==null||i({})},R=[];if(v<=7)for(let j=0;j<v;j++)R.push(j);else{R.push(0),b>2&&R.push("…");for(let j=Math.max(1,b-1);j<=Math.min(v-2,b+1);j++)R.push(j);b<v-3&&R.push("…"),R.push(v-1)}return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-3)",...s},children:[x&&e.jsx("div",{style:{position:"relative",zIndex:1},children:e.jsx(ea,{columns:t,rows:r,filters:g,onFilter:E,onClearAll:F})}),e.jsx("div",{style:{overflowX:"auto",borderRadius:"var(--lucent-radius-lg)",border:"1px solid var(--lucent-border-default)",background:"var(--lucent-surface)"},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-sm)"},children:[e.jsx("thead",{children:e.jsx("tr",{style:{borderBottom:"1px solid var(--lucent-border-default)"},children:t.map(j=>{const P=(d==null?void 0:d.key)===j.key?d.dir:"none";return e.jsx("th",{onClick:j.sortable?()=>T(j.key):void 0,style:{padding:"var(--lucent-space-3) var(--lucent-space-4)",textAlign:j.align??"left",fontWeight:"var(--lucent-font-weight-medium)",color:"var(--lucent-text-secondary)",background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",borderBottom:"1px solid var(--lucent-border-default)",cursor:j.sortable?"pointer":"default",userSelect:"none",whiteSpace:"nowrap",...j.width?{width:j.width}:{}},children:e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-1)"},children:[j.header,j.sortable&&e.jsx(Zr,{state:P}),j.headerFilter&&e.jsx("span",{onClick:D=>D.stopPropagation(),style:{display:"inline-flex",marginLeft:"var(--lucent-space-1)"},children:j.headerFilter})]})},j.key)})})}),e.jsx("tbody",{children:C.length===0?e.jsx("tr",{children:e.jsx("td",{colSpan:t.length,style:{padding:"var(--lucent-space-12)",textAlign:"center"},children:l??e.jsx(I.Text,{color:"secondary",children:"No data"})})}):C.map((j,P)=>e.jsx("tr",{onMouseEnter:()=>y(P),onMouseLeave:()=>y(null),style:{borderBottom:P<C.length-1?"1px solid var(--lucent-border-subtle)":"none",background:w===P?"color-mix(in srgb, var(--lucent-text-primary) 4%, transparent)":"transparent",transition:"background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:t.map(D=>e.jsx("td",{style:{padding:"var(--lucent-space-3) var(--lucent-space-4)",color:"var(--lucent-text-primary)",textAlign:D.align??"left",verticalAlign:"middle"},children:D.render?D.render(j,P):String(j[D.key]??"")},D.key))},P))})]})}),n>0&&S.length>0&&e.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"var(--lucent-space-3)",flexWrap:"wrap"},children:[e.jsx(I.Text,{color:"secondary",size:"sm",children:S.length===0?`0 rows${r.length>0?` (filtered from ${r.length})`:""}`:S.length===1?`1 row${S.length<r.length?` (filtered from ${r.length})`:""}`:`${b*n+1}–${Math.min((b+1)*n,S.length)} of ${S.length} rows${S.length<r.length?` (filtered from ${r.length})`:""}`}),e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-1)"},children:[e.jsx(ke,{onClick:()=>z(b-1),disabled:b===0,"aria-label":"Previous page",children:e.jsx(Xe,{dir:"left"})}),R.map((j,P)=>j==="…"?e.jsx("span",{style:{padding:"0 var(--lucent-space-1)",color:"var(--lucent-text-disabled)",fontSize:"var(--lucent-font-size-sm)"},children:"…"},`ellipsis-${P}`):e.jsx(ke,{onClick:()=>z(j),active:j===b,"aria-label":`Page ${j+1}`,"aria-current":j===b?"page":void 0,children:j+1},j)),e.jsx(ke,{onClick:()=>z(b+1),disabled:b>=v-1,"aria-label":"Next page",children:e.jsx(Xe,{dir:"right"})})]})]})]})}function ea({columns:t,rows:r,filters:n,onFilter:o,onClearAll:a}){const i=t.filter(s=>s.filterable),l=Object.keys(n).length;return e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-2)",flexWrap:"wrap"},children:[i.map(s=>{const d=Array.from(new Set(r.map(p=>String(p[s.key]??"")))).sort();return e.jsx(ta,{label:s.header,values:d,value:n[s.key]??[],onChange:p=>o(s.key,p)},s.key)}),l>0&&e.jsx("button",{onClick:a,style:{display:"inline-flex",alignItems:"center",height:30,padding:"0 var(--lucent-space-2)",border:"none",borderRadius:"var(--lucent-radius-md)",background:"transparent",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",cursor:"pointer"},children:"Clear all"})]})}function ta({label:t,values:r,value:n,onChange:o}){const[a,i]=c.useState(!1),[l,s]=c.useState(!1),[d,p]=c.useState(""),h=c.useRef(null),f=c.useRef(null),w=n.length>0;c.useEffect(()=>{if(!a){p("");return}setTimeout(()=>{var x;return(x=f.current)==null?void 0:x.focus()},0);const u=x=>{h.current&&!h.current.contains(x.target)&&i(!1)},b=x=>{x.key==="Escape"&&i(!1)};return document.addEventListener("mousedown",u),document.addEventListener("keydown",b),()=>{document.removeEventListener("mousedown",u),document.removeEventListener("keydown",b)}},[a]);const y=d?r.filter(u=>u.toLowerCase().includes(d.toLowerCase())):r,g=u=>o(n.includes(u)?n.filter(b=>b!==u):[...n,u]),m=n.length===0?null:n.length===1?e.jsxs("span",{style:{color:"var(--lucent-text-secondary)",fontWeight:"var(--lucent-font-weight-regular)"},children:[": ",n[0]]}):e.jsxs("span",{style:{color:"var(--lucent-accent-default)"},children:["(",n.length,")"]});return e.jsxs("div",{ref:h,style:{position:"relative"},children:[e.jsxs("button",{onClick:()=>i(u=>!u),onMouseEnter:()=>s(!0),onMouseLeave:()=>s(!1),style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-1)",height:30,padding:"0 var(--lucent-space-3)",borderRadius:"var(--lucent-radius-md)",border:`1px solid ${w?"var(--lucent-accent-default)":l?"var(--lucent-border-strong)":"var(--lucent-border-default)"}`,background:w?"var(--lucent-accent-subtle)":"var(--lucent-surface)",color:w?"var(--lucent-accent-default)":"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",fontWeight:w?"var(--lucent-font-weight-medium)":"var(--lucent-font-weight-regular)",cursor:"pointer",outline:"none",whiteSpace:"nowrap",transition:"border-color var(--lucent-duration-fast) var(--lucent-easing-default), background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:[t,m,e.jsx("svg",{width:"10",height:"10",viewBox:"0 0 10 10",fill:"none","aria-hidden":!0,style:{transform:a?"rotate(180deg)":"none",transition:"transform var(--lucent-duration-fast) var(--lucent-easing-default)"},children:e.jsx("path",{d:"M2 3.5L5 6.5L8 3.5",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})]}),a&&e.jsxs("div",{style:{position:"absolute",top:"calc(100% + 4px)",left:0,minWidth:180,maxHeight:280,display:"flex",flexDirection:"column",background:"var(--lucent-surface)",border:"1px solid var(--lucent-border-default)",borderRadius:"var(--lucent-radius-lg)",boxShadow:"0 4px 16px color-mix(in srgb, var(--lucent-text-primary) 8%, transparent)",zIndex:50},children:[e.jsx("div",{style:{padding:"var(--lucent-space-2)",paddingBottom:0},children:e.jsx("input",{ref:f,type:"text",value:d,onChange:u=>p(u.target.value),placeholder:"Search…",style:{width:"100%",boxSizing:"border-box",height:26,padding:"0 var(--lucent-space-2)",borderRadius:"var(--lucent-radius-md)",border:"1px solid var(--lucent-border-default)",background:"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",outline:"none"}})}),n.length>0&&e.jsx("div",{style:{padding:"var(--lucent-space-1) var(--lucent-space-2) 0"},children:e.jsx("button",{onClick:()=>o([]),style:{display:"inline-flex",padding:"2px var(--lucent-space-2)",border:"none",borderRadius:"var(--lucent-radius-sm)",background:"transparent",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",cursor:"pointer",textDecoration:"underline"},children:"Clear selection"})}),e.jsx("div",{style:{overflowY:"auto",padding:"var(--lucent-space-1)",borderTop:"1px solid var(--lucent-border-subtle)",marginTop:"var(--lucent-space-2)"},children:y.length===0?e.jsx("div",{style:{padding:"var(--lucent-space-3)",color:"var(--lucent-text-disabled)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",textAlign:"center"},children:"No results"}):y.map(u=>e.jsx(na,{label:u,isSelected:n.includes(u),onClick:()=>g(u)},u))})]})]})}function na({label:t,isSelected:r,onClick:n}){const[o,a]=c.useState(!1);return e.jsxs("button",{onClick:n,onMouseEnter:()=>a(!0),onMouseLeave:()=>a(!1),style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-2)",width:"100%",textAlign:"left",padding:"var(--lucent-space-2) var(--lucent-space-3)",borderRadius:"var(--lucent-radius-md)",border:"none",background:o?"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)":"transparent",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-xs)",fontWeight:r?"var(--lucent-font-weight-medium)":"var(--lucent-font-weight-regular)",cursor:"pointer",outline:"none",whiteSpace:"nowrap"},children:[e.jsx("span",{style:{flexShrink:0,width:14,height:14,borderRadius:"var(--lucent-radius-sm)",border:`1.5px solid ${r?"var(--lucent-accent-default)":"var(--lucent-border-strong)"}`,background:r?"var(--lucent-accent-default)":"transparent",display:"flex",alignItems:"center",justifyContent:"center",transition:"border-color var(--lucent-duration-fast), background var(--lucent-duration-fast)"},children:r&&e.jsx("svg",{width:"8",height:"8",viewBox:"0 0 8 8",fill:"none","aria-hidden":!0,children:e.jsx("path",{d:"M1 4L3 6L7 2",stroke:"var(--lucent-accent-fg)",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})}),t]})}function ke({children:t,onClick:r,disabled:n,active:o,...a}){const[i,l]=c.useState(!1);return e.jsx("button",{...a,onClick:r,disabled:n,onMouseEnter:()=>l(!0),onMouseLeave:()=>l(!1),style:{display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:32,height:32,padding:"0 var(--lucent-space-2)",borderRadius:"var(--lucent-radius-md)",border:o?"1px solid var(--lucent-accent-default)":"1px solid transparent",background:o?"var(--lucent-accent-default)":i&&!n?"color-mix(in srgb, var(--lucent-text-primary) 5%, transparent)":"transparent",color:o?"var(--lucent-accent-fg)":n?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",fontSize:"var(--lucent-font-size-sm)",fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-regular)",cursor:n?"not-allowed":"pointer",transition:"background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:t})}const ra={id:"data-table",name:"DataTable",tier:"molecule",domain:"neutral",specVersion:"1.0",description:"A sortable, filterable, paginated data table with configurable columns, custom cell renderers, and keyboard-accessible pagination controls.",designIntent:'DataTable is generic over row type T so TypeScript consumers get full type safety on column keys and renderers. Sorting is client-side and composable — each column opts in via sortable:true; clicking a sorted column cycles asc → desc → unsorted. Filtering is per-column — each column opts in via filterable:true, which adds a dropdown button above the table. Each dropdown is searchable and multi-select: a search input filters the option list, and each option is a checkbox that toggles membership in the active set. Filtering uses set-membership: a row passes if its column value is included in the selected values array. A "Clear selection" link inside each dropdown clears that column; a "Clear all" button in the bar appears when any filter is active. Filter → sort → paginate is the fixed pipeline order; any filter change resets the page to 0. Pagination is either controlled (page prop + onPageChange) or uncontrolled (internal state). A pageSize of 0 disables pagination entirely, useful when the parent manages windowing. Row hover uses bg-subtle, not a border change, so the visual weight stays low for dense data views. The table wrapper paints var(--lucent-surface) as its background so the component always sits on a solid panel, regardless of parent page color; header tints and row hover are translucent color-mix overlays on top of that surface.',props:[{name:"columns",type:"array",required:!0,description:"Column definitions. Each column has a key, header, optional render function, optional sortable flag, optional filterable flag (renders a text filter input below the header), optional width, and optional text align."},{name:"rows",type:"array",required:!0,description:"Array of data objects to display. The generic type T is inferred from this prop."},{name:"pageSize",type:"number",required:!1,default:"10",description:"Number of rows per page. Set to 0 to disable pagination."},{name:"page",type:"number",required:!1,description:"Controlled current page (0-indexed). When provided, the component is fully controlled."},{name:"onPageChange",type:"function",required:!1,description:"Called with the new page index whenever the page changes (from pagination controls or after a sort/filter reset)."},{name:"onFilterChange",type:"function",required:!1,description:"Called with the current filter map (Record<string, string[]>) whenever any column filter changes. Keys are column keys; columns with no selection are omitted from the map."},{name:"emptyState",type:"ReactNode",required:!1,description:'Content to render when rows is empty. Defaults to a "No data" text.'},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Basic sortable table",code:`<DataTable
343
343
  columns={[
344
344
  { key: 'name', header: 'Name', sortable: true },
345
345
  { key: 'role', header: 'Role', sortable: true },
@@ -402,7 +402,7 @@ const [results, setResults] = useState([]);
402
402
  min={new Date()}
403
403
  placeholder="Select a future date"
404
404
  />`}],compositionGraph:[{componentId:"text",componentName:"Text",role:"Month/year header and weekday labels",required:!0}],accessibility:{role:"dialog",ariaAttributes:["aria-haspopup","aria-expanded","aria-invalid","aria-label","aria-pressed"],keyboardInteractions:["Enter/Space to open calendar","Click day to select","Escape closes popover (click outside)"],notes:'The calendar popover is role="dialog". Each day button has aria-label with the full date and aria-pressed for selected state. Full arrow-key navigation within the calendar grid is a planned enhancement.'}},Ea={sm:"calc(var(--lucent-space-8) * 0.5 + 18px)",md:"calc(var(--lucent-space-10) * 0.5 + 22px)",lg:"calc(var(--lucent-space-12) * 0.5 + 26px)"},Aa={sm:"var(--lucent-font-size-sm)",md:"var(--lucent-font-size-md)",lg:"var(--lucent-font-size-md)"},qa={sm:"var(--lucent-space-3)",md:"var(--lucent-space-4)",lg:"var(--lucent-space-4)"},Da={sm:"var(--lucent-space-2)",md:"calc((var(--lucent-space-2) + var(--lucent-space-3)) / 2)",lg:"var(--lucent-space-3)"},Ce={sm:"var(--lucent-font-size-sm)",md:"var(--lucent-font-size-sm)",lg:"var(--lucent-font-size-md)"};function Ra(t,r){return t?ge(t.start,t.end)?ie(t.start):`${ie(t.start)} → ${ie(t.end)}`:r}function vt({value:t,defaultValue:r,onChange:n,placeholder:o="Pick a date range",disabled:a=!1,min:i,max:l,size:s="md",label:d,helperText:p,errorText:h,trigger:f,style:w}){const y=t!==void 0,[g,m]=c.useState(r),u=y?t:g,b=!!h,x=a,k=`lucent-daterangepicker-${Math.random().toString(36).slice(2,7)}`,[S,C]=c.useState(null),[v,z]=c.useState(null),T=new Date,[E,F]=c.useState(((u==null?void 0:u.start)??T).getFullYear()),[R,j]=c.useState(((u==null?void 0:u.start)??T).getMonth()),P=R===11?0:R+1,D=R===11?E+1:E,[W,q]=c.useState(!1),[N,H]=c.useState(!1),A=c.useRef(null),B=c.useRef(null),[M,$]=c.useState({top:0,left:0});c.useEffect(()=>{if(!W)return;const O=Q=>{var ce,re;!((ce=A.current)!=null&&ce.contains(Q.target))&&!((re=B.current)!=null&&re.contains(Q.target))&&(q(!1),C(null))};return document.addEventListener("mousedown",O),()=>document.removeEventListener("mousedown",O)},[W]),c.useLayoutEffect(()=>{if(!W||!A.current)return;const O=A.current.getBoundingClientRect();$({top:O.bottom+4,left:O.left})},[W]);const V=O=>{if(!S)C(O);else{const[Q,ce]=ve(O,S)||ge(O,S)?[O,S]:[S,O],re={start:Q,end:ce};y||m(re),n==null||n(re),C(null),q(!1)}},L=()=>{R===0?(j(11),F(O=>O-1)):j(O=>O-1)},G=()=>{R===11?(j(0),F(O=>O+1)):j(O=>O+1)};let U;if(S&&v){const[O,Q]=ve(v,S)?[v,S]:[S,v];U={start:O,end:Q}}else S?U={start:S,end:S}:u&&(U={start:u.start,end:u.end});const Z=x?"transparent":b?"var(--lucent-danger-default)":N?"var(--lucent-accent-border)":"var(--lucent-border-default)",K=N?`0 0 0 3px ${b?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`:"none";return e.jsxs("div",{ref:A,style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",...w},children:[d&&e.jsx("label",{htmlFor:k,style:{fontSize:Ce[s],fontWeight:"var(--lucent-font-weight-medium)",color:x?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)"},children:d}),f?e.jsx("span",{onClick:()=>!a&&q(O=>!O),"aria-haspopup":"dialog","aria-expanded":W,style:{display:"inline-flex",cursor:a?"not-allowed":"pointer"},children:f}):e.jsxs("button",{type:"button",id:k,disabled:a,onClick:()=>!a&&q(O=>!O),onFocus:()=>H(!0),onBlur:()=>H(!1),"aria-haspopup":"dialog","aria-expanded":W,"aria-invalid":b,style:{display:"flex",alignItems:"center",gap:Da[s],width:"100%",height:Ea[s],boxSizing:"border-box",padding:`0 ${qa[s]}`,borderRadius:"var(--lucent-radius-lg)",border:`1px solid ${Z}`,boxShadow:K,background:x?"color-mix(in srgb, var(--lucent-text-primary) 6%, transparent)":"var(--lucent-surface)",color:u?"var(--lucent-text-primary)":"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)",fontSize:Aa[s],cursor:x?"not-allowed":"pointer",outline:"none",transition:["border-color var(--lucent-duration-fast) var(--lucent-easing-default)","box-shadow var(--lucent-duration-fast) var(--lucent-easing-default)"].join(", ")},children:[e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none","aria-hidden":!0,style:{flexShrink:0},children:[e.jsx("rect",{x:"1",y:"2",width:"12",height:"11",rx:"2",stroke:"currentColor",strokeWidth:"1.3"}),e.jsx("path",{d:"M1 6h12",stroke:"currentColor",strokeWidth:"1.3"}),e.jsx("path",{d:"M4 1v2M10 1v2",stroke:"currentColor",strokeWidth:"1.3",strokeLinecap:"round"})]}),e.jsx("span",{style:{flex:1,textAlign:"left"},children:Ra(u,o)})]}),b&&e.jsx("span",{role:"alert",style:{fontSize:Ce[s],color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:h}),!b&&p&&e.jsx("span",{style:{fontSize:Ce[s],color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)"},children:p}),W&&J.createPortal(e.jsxs("div",{ref:B,role:"dialog","aria-label":"Date range picker",style:{position:"fixed",top:M.top,left:M.left,zIndex:1e3,background:"color-mix(in srgb, var(--lucent-surface-overlay) 85%, transparent)",backdropFilter:"blur(6px)",WebkitBackdropFilter:"blur(6px)",border:"1px solid color-mix(in srgb, var(--lucent-accent-default) 15%, var(--lucent-border-default))",borderRadius:"var(--lucent-radius-lg)",boxShadow:"0 0 24px -4px color-mix(in srgb, var(--lucent-accent-default) 12%, transparent), var(--lucent-shadow-md)",padding:gt[s],display:"flex",gap:"var(--lucent-space-6)"},children:[e.jsx("div",{style:{minWidth:qe[s]},children:e.jsx(De,{year:E,month:R,...(u==null?void 0:u.start)!==void 0&&{selected:u.start},today:T,...i!==void 0&&{min:i},...l!==void 0&&{max:l},onSelect:V,onPrevMonth:L,onNextMonth:G,...U!==void 0&&{highlightRange:U},...S&&{onDayHover:z},size:s})}),e.jsx("div",{style:{width:1,background:"var(--lucent-border-subtle)",flexShrink:0}}),e.jsx("div",{style:{minWidth:qe[s]},children:e.jsx(De,{year:D,month:P,...(u==null?void 0:u.end)!==void 0&&{selected:u.end},today:T,...i!==void 0&&{min:i},...l!==void 0&&{max:l},onSelect:V,onPrevMonth:L,onNextMonth:G,...U!==void 0&&{highlightRange:U},...S&&{onDayHover:z},size:s})})]}),document.body),S&&W&&e.jsx("div",{style:{position:"absolute",top:"calc(100% + var(--lucent-space-1))",left:0,zIndex:1001,pointerEvents:"none"}}),S&&W&&e.jsx("div",{style:{position:"absolute",bottom:-24,left:0},children:e.jsx(I.Text,{size:"xs",color:"secondary",children:"Now pick the end date"})})]})}const Ba={id:"date-range-picker",name:"DateRangePicker",tier:"molecule",domain:"neutral",specVersion:"1.0",description:"A two-calendar date range picker. First click sets the start date, second click sets the end; the selected interval is highlighted across both calendars.",designIntent:'DateRangePicker composes two Calendar primitives from DatePicker side by side, advancing in lockstep (left = current month, right = next month). Selection is a two-click flow: first click anchors the start, a hint appears ("Now pick the end date"), second click resolves the range with automatic start/end ordering so users can click in either direction. The highlight range (accent-subtle background) spans both calendars to give clear visual feedback for the selected interval. Navigation (prev/next month) advances both calendars together to maintain the one-month-apart constraint.',props:[{name:"value",type:"object",required:!1,description:"Controlled DateRange { start: Date; end: Date }. When provided the component is fully controlled."},{name:"defaultValue",type:"object",required:!1,description:"Initial DateRange for uncontrolled usage."},{name:"onChange",type:"function",required:!1,description:"Called with the completed DateRange after the user picks both start and end."},{name:"placeholder",type:"string",required:!1,default:'"Pick a date range"',description:"Trigger button text when no range is selected."},{name:"size",type:"enum",required:!1,default:"md",description:"Controls trigger height and font size to match Input/Select.",enumValues:["sm","md","lg"]},{name:"label",type:"string",required:!1,description:"Label text rendered above the trigger, with size-aware font sizing matching Input/Select."},{name:"helperText",type:"string",required:!1,description:"Helper text rendered below the trigger. Hidden when errorText is present."},{name:"errorText",type:"string",required:!1,description:"Error text rendered below the trigger in danger color. Takes precedence over helperText."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the trigger button and all interaction."},{name:"min",type:"object",required:!1,description:"Earliest selectable Date."},{name:"max",type:"object",required:!1,description:"Latest selectable Date."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Uncontrolled",code:"<DateRangePicker onChange={({ start, end }) => console.log(start, end)} />"},{title:"Controlled",code:`const [range, setRange] = useState<DateRange>();
405
- <DateRangePicker value={range} onChange={setRange} min={new Date()} />`}],compositionGraph:[{componentId:"date-picker",componentName:"DatePicker",role:"Calendar primitive (two instances, left and right)",required:!0},{componentId:"text",componentName:"Text",role:"Mid-selection hint and calendar headers",required:!0}],accessibility:{role:"dialog",ariaAttributes:["aria-haspopup","aria-expanded","aria-invalid","aria-label","aria-pressed"],keyboardInteractions:["Enter/Space to open","Click first day to set start","Click second day to set end","Escape/click outside to cancel"],notes:'Inherits Calendar accessibility from DatePicker. The two-step selection flow is reinforced with a visible "Now pick the end date" hint.'}};function Re(t){return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Pa(){return Math.random().toString(36).slice(2)}function La({item:t,onRemove:r}){const[n,o]=c.useState(!1),a=t.progress,i=!!t.error;return e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-3)",padding:"var(--lucent-space-2) var(--lucent-space-3)",borderRadius:"var(--lucent-radius-md)",border:`1px solid ${i?"var(--lucent-danger-default)":"var(--lucent-border-default)"}`,background:"var(--lucent-surface)"},children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 20 20",fill:"none","aria-hidden":!0,style:{flexShrink:0,color:"var(--lucent-text-secondary)"},children:[e.jsx("path",{d:"M5 2h7l4 4v12a1 1 0 01-1 1H5a1 1 0 01-1-1V3a1 1 0 011-1z",stroke:"currentColor",strokeWidth:"1.3"}),e.jsx("path",{d:"M12 2v4h4",stroke:"currentColor",strokeWidth:"1.3"})]}),e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsx(I.Text,{size:"sm",truncate:!0,children:t.file.name}),i?e.jsx(I.Text,{size:"xs",color:"danger",children:t.error}):e.jsx(I.Text,{size:"xs",color:"secondary",children:Re(t.file.size)}),a!==void 0&&!i&&e.jsx("div",{style:{marginTop:4,height:3,borderRadius:"var(--lucent-radius-full)",background:"var(--lucent-surface-secondary)",overflow:"hidden"},children:e.jsx("div",{style:{height:"100%",width:`${a}%`,borderRadius:"var(--lucent-radius-full)",background:a===100?"var(--lucent-success-default)":"var(--lucent-accent-default)",transition:"width 200ms var(--lucent-easing-default)"}})})]}),e.jsx("button",{type:"button",onClick:()=>r(t.id),onMouseEnter:()=>o(!0),onMouseLeave:()=>o(!1),"aria-label":`Remove ${t.file.name}`,style:{flexShrink:0,display:"inline-flex",alignItems:"center",justifyContent:"center",width:24,height:24,border:"none",borderRadius:"var(--lucent-radius-md)",background:n?"var(--lucent-surface-secondary)":"transparent",color:"var(--lucent-text-secondary)",cursor:"pointer",transition:"background var(--lucent-duration-fast)"},children:e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 12 12",fill:"none","aria-hidden":!0,children:e.jsx("path",{d:"M2 2l8 8M10 2l-8 8",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})})})]})}function Fa({accept:t,multiple:r=!1,maxSize:n,value:o,onChange:a,onError:i,disabled:l=!1,style:s}){const d=o!==void 0,[p,h]=c.useState([]),f=d?o:p,[w,y]=c.useState(!1),[g,m]=c.useState(!1),u=c.useRef(null),b=c.useCallback(C=>{if(!C||l)return;const v=[];for(const T of Array.from(C)){if(n&&T.size>n){i==null||i(`"${T.name}" exceeds the ${Re(n)} limit.`);continue}if(!r&&f.length+v.length>=1)break;v.push({id:Pa(),file:T})}if(v.length===0)return;const z=r?[...f,...v]:v;d||h(z),a==null||a(z)},[l,f,d,n,r,a,i]),x=C=>{const v=f.filter(z=>z.id!==C);d||h(v),a==null||a(v)},k=C=>{C.preventDefault(),y(!1),b(C.dataTransfer.files)},S=C=>{b(C.target.files),C.target.value=""};return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-3)",...s},children:[e.jsxs("div",{role:"button",tabIndex:l?-1:0,"aria-label":"Upload files","aria-disabled":l,onClick:()=>{var C;return!l&&((C=u.current)==null?void 0:C.click())},onKeyDown:C=>{var v;(C.key==="Enter"||C.key===" ")&&(C.preventDefault(),(v=u.current)==null||v.click())},onFocus:()=>m(!0),onBlur:()=>m(!1),onDragOver:C=>{C.preventDefault(),l||y(!0)},onDragLeave:()=>y(!1),onDrop:k,style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:"var(--lucent-space-2)",padding:"var(--lucent-space-8) var(--lucent-space-6)",borderRadius:"var(--lucent-radius-lg)",border:`2px dashed ${l?"var(--lucent-border-default)":w||g?"var(--lucent-accent-default)":"var(--lucent-border-default)"}`,background:w?"var(--lucent-accent-subtle)":"var(--lucent-surface-secondary)",cursor:l?"not-allowed":"pointer",transition:"border-color var(--lucent-duration-fast), background var(--lucent-duration-fast)",outline:"none"},children:[e.jsxs("svg",{width:"32",height:"32",viewBox:"0 0 32 32",fill:"none","aria-hidden":!0,style:{color:l?"var(--lucent-text-disabled)":w?"var(--lucent-accent-default)":"var(--lucent-text-secondary)"},children:[e.jsx("path",{d:"M16 20V10M16 10l-4 4M16 10l4 4",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",strokeLinejoin:"round"}),e.jsx("path",{d:"M8 24h16",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round"})]}),e.jsxs("div",{style:{textAlign:"center",width:"100%"},children:[e.jsx(I.Text,{color:l?"disabled":"primary",weight:"medium",align:"center",children:w?"Drop to upload":"Drop files here or click to browse"}),(t||n)&&e.jsx(I.Text,{size:"xs",color:"secondary",align:"center",children:[t&&`Accepted: ${t}`,n&&`Max size: ${Re(n)}`].filter(Boolean).join(" · ")})]}),e.jsx("input",{ref:u,type:"file",accept:t,multiple:r,disabled:l,onChange:S,style:{display:"none"},tabIndex:-1})]}),f.length>0&&e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-2)"},children:f.map(C=>e.jsx(La,{item:C,onRemove:x},C.id))})]})}const Na={id:"file-upload",name:"FileUpload",tier:"molecule",domain:"neutral",specVersion:"1.0",description:"A drag-and-drop file upload zone with a file list, per-file progress bars, error display, and size/type validation.",designIntent:'FileUpload separates concerns between the drop zone (entry point) and the file list (status display). The drop zone uses a dashed border and an upload arrow icon to communicate droppability without words. Progress is modelled as a field on UploadFile rather than as a callback so the parent controls upload logic — this component is purely presentational for the upload state. The progress bar turns success-green at 100% to give clear completion feedback. Errors are shown inline on each file row (not as a toast) so the user knows exactly which file failed and why. The hidden <input type="file"> is triggered programmatically on click/keyboard so the drop zone can have a fully custom appearance.',props:[{name:"accept",type:"string",required:!1,description:'Accepted MIME types or extensions passed to the file input, e.g. "image/*,.pdf".'},{name:"multiple",type:"boolean",required:!1,default:"false",description:"Allow selecting multiple files at once."},{name:"maxSize",type:"number",required:!1,description:"Maximum file size in bytes. Files exceeding this trigger onError and are not added."},{name:"value",type:"array",required:!1,description:"Controlled array of UploadFile objects. Each has id, file (File), optional progress (0–100), and optional error string."},{name:"onChange",type:"function",required:!1,description:"Called with the updated UploadFile array after files are added or removed."},{name:"onError",type:"function",required:!1,description:"Called with an error message string when a file fails validation (e.g. exceeds maxSize)."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the drop zone and all file interaction."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Uncontrolled multi-file upload",code:`<FileUpload
405
+ <DateRangePicker value={range} onChange={setRange} min={new Date()} />`}],compositionGraph:[{componentId:"date-picker",componentName:"DatePicker",role:"Calendar primitive (two instances, left and right)",required:!0},{componentId:"text",componentName:"Text",role:"Mid-selection hint and calendar headers",required:!0}],accessibility:{role:"dialog",ariaAttributes:["aria-haspopup","aria-expanded","aria-invalid","aria-label","aria-pressed"],keyboardInteractions:["Enter/Space to open","Click first day to set start","Click second day to set end","Escape/click outside to cancel"],notes:'Inherits Calendar accessibility from DatePicker. The two-step selection flow is reinforced with a visible "Now pick the end date" hint.'}};function Re(t){return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Pa(){return Math.random().toString(36).slice(2)}function La({item:t,onRemove:r}){const[n,o]=c.useState(!1),a=t.progress,i=!!t.error;return e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-3)",padding:"var(--lucent-space-2) var(--lucent-space-3)",borderRadius:"var(--lucent-radius-md)",border:`1px solid ${i?"var(--lucent-danger-default)":"var(--lucent-border-default)"}`,background:"var(--lucent-surface)"},children:[e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 20 20",fill:"none","aria-hidden":!0,style:{flexShrink:0,color:"var(--lucent-text-secondary)"},children:[e.jsx("path",{d:"M5 2h7l4 4v12a1 1 0 01-1 1H5a1 1 0 01-1-1V3a1 1 0 011-1z",stroke:"currentColor",strokeWidth:"1.3"}),e.jsx("path",{d:"M12 2v4h4",stroke:"currentColor",strokeWidth:"1.3"})]}),e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsx(I.Text,{size:"sm",truncate:!0,children:t.file.name}),i?e.jsx(I.Text,{size:"xs",color:"danger",children:t.error}):e.jsx(I.Text,{size:"xs",color:"secondary",children:Re(t.file.size)}),a!==void 0&&!i&&e.jsx("div",{style:{marginTop:4,height:3,borderRadius:"var(--lucent-radius-full)",background:"color-mix(in srgb, var(--lucent-text-primary) 8%, transparent)",overflow:"hidden"},children:e.jsx("div",{style:{height:"100%",width:`${a}%`,borderRadius:"var(--lucent-radius-full)",background:a===100?"var(--lucent-success-default)":"var(--lucent-accent-default)",transition:"width 200ms var(--lucent-easing-default)"}})})]}),e.jsx("button",{type:"button",onClick:()=>r(t.id),onMouseEnter:()=>o(!0),onMouseLeave:()=>o(!1),"aria-label":`Remove ${t.file.name}`,style:{flexShrink:0,display:"inline-flex",alignItems:"center",justifyContent:"center",width:24,height:24,border:"none",borderRadius:"var(--lucent-radius-md)",background:n?"color-mix(in srgb, var(--lucent-text-primary) 6%, transparent)":"transparent",color:"var(--lucent-text-secondary)",cursor:"pointer",transition:"background var(--lucent-duration-fast)"},children:e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 12 12",fill:"none","aria-hidden":!0,children:e.jsx("path",{d:"M2 2l8 8M10 2l-8 8",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})})})]})}function Fa({accept:t,multiple:r=!1,maxSize:n,value:o,onChange:a,onError:i,disabled:l=!1,style:s}){const d=o!==void 0,[p,h]=c.useState([]),f=d?o:p,[w,y]=c.useState(!1),[g,m]=c.useState(!1),u=c.useRef(null),b=c.useCallback(C=>{if(!C||l)return;const v=[];for(const T of Array.from(C)){if(n&&T.size>n){i==null||i(`"${T.name}" exceeds the ${Re(n)} limit.`);continue}if(!r&&f.length+v.length>=1)break;v.push({id:Pa(),file:T})}if(v.length===0)return;const z=r?[...f,...v]:v;d||h(z),a==null||a(z)},[l,f,d,n,r,a,i]),x=C=>{const v=f.filter(z=>z.id!==C);d||h(v),a==null||a(v)},k=C=>{C.preventDefault(),y(!1),b(C.dataTransfer.files)},S=C=>{b(C.target.files),C.target.value=""};return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-3)",...s},children:[e.jsxs("div",{role:"button",tabIndex:l?-1:0,"aria-label":"Upload files","aria-disabled":l,onClick:()=>{var C;return!l&&((C=u.current)==null?void 0:C.click())},onKeyDown:C=>{var v;(C.key==="Enter"||C.key===" ")&&(C.preventDefault(),(v=u.current)==null||v.click())},onFocus:()=>m(!0),onBlur:()=>m(!1),onDragOver:C=>{C.preventDefault(),l||y(!0)},onDragLeave:()=>y(!1),onDrop:k,style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:"var(--lucent-space-2)",padding:"var(--lucent-space-8) var(--lucent-space-6)",borderRadius:"var(--lucent-radius-lg)",border:`2px dashed ${l?"color-mix(in srgb, var(--lucent-text-primary) 15%, transparent)":w||g?"var(--lucent-accent-default)":"color-mix(in srgb, var(--lucent-text-primary) 20%, transparent)"}`,background:w?"var(--lucent-accent-subtle)":"color-mix(in srgb, var(--lucent-text-primary) 4%, transparent)",cursor:l?"not-allowed":"pointer",transition:"border-color var(--lucent-duration-fast), background var(--lucent-duration-fast)",outline:"none"},children:[e.jsxs("svg",{width:"32",height:"32",viewBox:"0 0 32 32",fill:"none","aria-hidden":!0,style:{color:l?"var(--lucent-text-disabled)":w?"var(--lucent-accent-default)":"var(--lucent-text-secondary)"},children:[e.jsx("path",{d:"M16 20V10M16 10l-4 4M16 10l4 4",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",strokeLinejoin:"round"}),e.jsx("path",{d:"M8 24h16",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round"})]}),e.jsxs("div",{style:{textAlign:"center",width:"100%"},children:[e.jsx(I.Text,{color:l?"disabled":"primary",weight:"medium",align:"center",children:w?"Drop to upload":"Drop files here or click to browse"}),(t||n)&&e.jsx(I.Text,{size:"xs",color:"secondary",align:"center",children:[t&&`Accepted: ${t}`,n&&`Max size: ${Re(n)}`].filter(Boolean).join(" · ")})]}),e.jsx("input",{ref:u,type:"file",accept:t,multiple:r,disabled:l,onChange:S,style:{display:"none"},tabIndex:-1})]}),f.length>0&&e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-2)"},children:f.map(C=>e.jsx(La,{item:C,onRemove:x},C.id))})]})}const Na={id:"file-upload",name:"FileUpload",tier:"molecule",domain:"neutral",specVersion:"1.0",description:"A drag-and-drop file upload zone with a file list, per-file progress bars, error display, and size/type validation.",designIntent:'FileUpload separates concerns between the drop zone (entry point) and the file list (status display). The drop zone uses a dashed border and an upload arrow icon to communicate droppability without words. Its idle background and border are translucent color-mix overlays on top of var(--lucent-text-primary) (not hard-coded surface tokens), so the drop zone always reads as a visible step against whatever parent background it sits on — in any theme, in any palette. Progress is modelled as a field on UploadFile rather than as a callback so the parent controls upload logic — this component is purely presentational for the upload state. The progress bar turns success-green at 100% to give clear completion feedback. Errors are shown inline on each file row (not as a toast) so the user knows exactly which file failed and why. The hidden <input type="file"> is triggered programmatically on click/keyboard so the drop zone can have a fully custom appearance.',props:[{name:"accept",type:"string",required:!1,description:'Accepted MIME types or extensions passed to the file input, e.g. "image/*,.pdf".'},{name:"multiple",type:"boolean",required:!1,default:"false",description:"Allow selecting multiple files at once."},{name:"maxSize",type:"number",required:!1,description:"Maximum file size in bytes. Files exceeding this trigger onError and are not added."},{name:"value",type:"array",required:!1,description:"Controlled array of UploadFile objects. Each has id, file (File), optional progress (0–100), and optional error string."},{name:"onChange",type:"function",required:!1,description:"Called with the updated UploadFile array after files are added or removed."},{name:"onError",type:"function",required:!1,description:"Called with an error message string when a file fails validation (e.g. exceeds maxSize)."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the drop zone and all file interaction."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Uncontrolled multi-file upload",code:`<FileUpload
406
406
  multiple
407
407
  accept="image/*,.pdf"
408
408
  maxSize={5 * 1024 * 1024}
package/dist/index.js CHANGED
@@ -2542,7 +2542,7 @@ function me({ striped: t = !1, children: r, className: n, style: o, ...a }) {
2542
2542
  ].filter(Boolean).join(" ");
2543
2543
  return /* @__PURE__ */ g(Q, { children: [
2544
2544
  /* @__PURE__ */ e("style", { children: sn }),
2545
- /* @__PURE__ */ e("div", { style: { overflowX: "auto", width: "100%" }, children: /* @__PURE__ */ e(
2545
+ /* @__PURE__ */ e("div", { style: { overflowX: "auto", width: "100%", background: "var(--lucent-surface)" }, children: /* @__PURE__ */ e(
2546
2546
  "table",
2547
2547
  {
2548
2548
  className: i,
@@ -2571,7 +2571,7 @@ const po = {
2571
2571
  domain: "neutral",
2572
2572
  specVersion: "0.1",
2573
2573
  description: "A lightweight, token-styled HTML table primitive with compound sub-components. Distinct from DataTable — no sorting, filtering, or pagination.",
2574
- designIntent: "Use Table for static or lightly dynamic tabular data where full DataTable features are not needed — props tables, changelog entries, comparison grids, reference docs. The compound API (Table.Head, Table.Body, Table.Row, Table.Cell) maps directly to semantic HTML so screen readers get the full table structure. Horizontal overflow is handled automatically by a scroll wrapper.",
2574
+ designIntent: "Use Table for static or lightly dynamic tabular data where full DataTable features are not needed — props tables, changelog entries, comparison grids, reference docs. The compound API (Table.Head, Table.Body, Table.Row, Table.Cell) maps directly to semantic HTML so screen readers get the full table structure. Horizontal overflow is handled automatically by a scroll wrapper. The wrapper paints var(--lucent-surface) as its background so the table always sits on a solid panel, regardless of parent page color; thead/tfoot/striped tints are translucent color-mix overlays on top of that surface so they adapt to both light and dark modes.",
2575
2575
  props: [
2576
2576
  {
2577
2577
  name: "striped",
@@ -5931,7 +5931,8 @@ function $o({
5931
5931
  /* @__PURE__ */ e("div", { style: {
5932
5932
  overflowX: "auto",
5933
5933
  borderRadius: "var(--lucent-radius-lg)",
5934
- border: "1px solid var(--lucent-border-default)"
5934
+ border: "1px solid var(--lucent-border-default)",
5935
+ background: "var(--lucent-surface)"
5935
5936
  }, children: /* @__PURE__ */ g("table", { style: {
5936
5937
  width: "100%",
5937
5938
  borderCollapse: "collapse",
@@ -6338,7 +6339,7 @@ const Wo = {
6338
6339
  domain: "neutral",
6339
6340
  specVersion: "1.0",
6340
6341
  description: "A sortable, filterable, paginated data table with configurable columns, custom cell renderers, and keyboard-accessible pagination controls.",
6341
- designIntent: 'DataTable is generic over row type T so TypeScript consumers get full type safety on column keys and renderers. Sorting is client-side and composable — each column opts in via sortable:true; clicking a sorted column cycles asc → desc → unsorted. Filtering is per-column — each column opts in via filterable:true, which adds a dropdown button above the table. Each dropdown is searchable and multi-select: a search input filters the option list, and each option is a checkbox that toggles membership in the active set. Filtering uses set-membership: a row passes if its column value is included in the selected values array. A "Clear selection" link inside each dropdown clears that column; a "Clear all" button in the bar appears when any filter is active. Filter → sort → paginate is the fixed pipeline order; any filter change resets the page to 0. Pagination is either controlled (page prop + onPageChange) or uncontrolled (internal state). A pageSize of 0 disables pagination entirely, useful when the parent manages windowing. Row hover uses bg-subtle, not a border change, so the visual weight stays low for dense data views.',
6342
+ designIntent: 'DataTable is generic over row type T so TypeScript consumers get full type safety on column keys and renderers. Sorting is client-side and composable — each column opts in via sortable:true; clicking a sorted column cycles asc → desc → unsorted. Filtering is per-column — each column opts in via filterable:true, which adds a dropdown button above the table. Each dropdown is searchable and multi-select: a search input filters the option list, and each option is a checkbox that toggles membership in the active set. Filtering uses set-membership: a row passes if its column value is included in the selected values array. A "Clear selection" link inside each dropdown clears that column; a "Clear all" button in the bar appears when any filter is active. Filter → sort → paginate is the fixed pipeline order; any filter change resets the page to 0. Pagination is either controlled (page prop + onPageChange) or uncontrolled (internal state). A pageSize of 0 disables pagination entirely, useful when the parent manages windowing. Row hover uses bg-subtle, not a border change, so the visual weight stays low for dense data views. The table wrapper paints var(--lucent-surface) as its background so the component always sits on a solid panel, regardless of parent page color; header tints and row hover are translucent color-mix overlays on top of that surface.',
6342
6343
  props: [
6343
6344
  {
6344
6345
  name: "columns",
@@ -7894,7 +7895,7 @@ function Vr({
7894
7895
  marginTop: 4,
7895
7896
  height: 3,
7896
7897
  borderRadius: "var(--lucent-radius-full)",
7897
- background: "var(--lucent-surface-secondary)",
7898
+ background: "color-mix(in srgb, var(--lucent-text-primary) 8%, transparent)",
7898
7899
  overflow: "hidden"
7899
7900
  }, children: /* @__PURE__ */ e("div", { style: {
7900
7901
  height: "100%",
@@ -7921,7 +7922,7 @@ function Vr({
7921
7922
  height: 24,
7922
7923
  border: "none",
7923
7924
  borderRadius: "var(--lucent-radius-md)",
7924
- background: n ? "var(--lucent-surface-secondary)" : "transparent",
7925
+ background: n ? "color-mix(in srgb, var(--lucent-text-primary) 6%, transparent)" : "transparent",
7925
7926
  color: "var(--lucent-text-secondary)",
7926
7927
  cursor: "pointer",
7927
7928
  transition: "background var(--lucent-duration-fast)"
@@ -7994,8 +7995,8 @@ function Yo({
7994
7995
  gap: "var(--lucent-space-2)",
7995
7996
  padding: "var(--lucent-space-8) var(--lucent-space-6)",
7996
7997
  borderRadius: "var(--lucent-radius-lg)",
7997
- border: `2px dashed ${l ? "var(--lucent-border-default)" : w || m ? "var(--lucent-accent-default)" : "var(--lucent-border-default)"}`,
7998
- background: w ? "var(--lucent-accent-subtle)" : "var(--lucent-surface-secondary)",
7998
+ border: `2px dashed ${l ? "color-mix(in srgb, var(--lucent-text-primary) 15%, transparent)" : w || m ? "var(--lucent-accent-default)" : "color-mix(in srgb, var(--lucent-text-primary) 20%, transparent)"}`,
7999
+ background: w ? "var(--lucent-accent-subtle)" : "color-mix(in srgb, var(--lucent-text-primary) 4%, transparent)",
7999
8000
  cursor: l ? "not-allowed" : "pointer",
8000
8001
  transition: "border-color var(--lucent-duration-fast), background var(--lucent-duration-fast)",
8001
8002
  outline: "none"
@@ -8049,7 +8050,7 @@ const Ko = {
8049
8050
  domain: "neutral",
8050
8051
  specVersion: "1.0",
8051
8052
  description: "A drag-and-drop file upload zone with a file list, per-file progress bars, error display, and size/type validation.",
8052
- designIntent: 'FileUpload separates concerns between the drop zone (entry point) and the file list (status display). The drop zone uses a dashed border and an upload arrow icon to communicate droppability without words. Progress is modelled as a field on UploadFile rather than as a callback so the parent controls upload logic — this component is purely presentational for the upload state. The progress bar turns success-green at 100% to give clear completion feedback. Errors are shown inline on each file row (not as a toast) so the user knows exactly which file failed and why. The hidden <input type="file"> is triggered programmatically on click/keyboard so the drop zone can have a fully custom appearance.',
8053
+ designIntent: 'FileUpload separates concerns between the drop zone (entry point) and the file list (status display). The drop zone uses a dashed border and an upload arrow icon to communicate droppability without words. Its idle background and border are translucent color-mix overlays on top of var(--lucent-text-primary) (not hard-coded surface tokens), so the drop zone always reads as a visible step against whatever parent background it sits on — in any theme, in any palette. Progress is modelled as a field on UploadFile rather than as a callback so the parent controls upload logic — this component is purely presentational for the upload state. The progress bar turns success-green at 100% to give clear completion feedback. Errors are shown inline on each file row (not as a toast) so the user knows exactly which file failed and why. The hidden <input type="file"> is triggered programmatically on click/keyboard so the drop zone can have a fully custom appearance.',
8053
8054
  props: [
8054
8055
  {
8055
8056
  name: "accept",
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from 'node:http';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { registerTools, DESIGN_RULES_SUMMARY } from './tools.js';
6
+ const PORT = Number(process.env['PORT'] ?? 3000);
7
+ const HOST = process.env['HOST'] ?? '127.0.0.1';
8
+ const API_KEY = process.env['LUCENT_API_KEY'];
9
+ const MCP_PATH = process.env['LUCENT_MCP_PATH'] ?? '/mcp';
10
+ function log(msg) {
11
+ process.stderr.write(`[lucent-mcp-http] ${msg}\n`);
12
+ }
13
+ // Build a fresh McpServer + tool registrations per request (stateless mode).
14
+ function createServerInstance() {
15
+ const server = new McpServer({ name: 'lucent-mcp', version: '0.1.0' }, { instructions: DESIGN_RULES_SUMMARY });
16
+ registerTools(server);
17
+ return server;
18
+ }
19
+ function readJsonBody(req) {
20
+ return new Promise((resolve, reject) => {
21
+ const chunks = [];
22
+ req.on('data', (chunk) => chunks.push(chunk));
23
+ req.on('end', () => {
24
+ const raw = Buffer.concat(chunks).toString('utf8');
25
+ if (!raw)
26
+ return resolve(undefined);
27
+ try {
28
+ resolve(JSON.parse(raw));
29
+ }
30
+ catch (err) {
31
+ reject(err instanceof Error ? err : new Error(String(err)));
32
+ }
33
+ });
34
+ req.on('error', reject);
35
+ });
36
+ }
37
+ function writeJson(res, status, body) {
38
+ res.writeHead(status, { 'Content-Type': 'application/json' });
39
+ res.end(JSON.stringify(body));
40
+ }
41
+ function jsonRpcError(res, status, code, message) {
42
+ writeJson(res, status, {
43
+ jsonrpc: '2.0',
44
+ error: { code, message },
45
+ id: null,
46
+ });
47
+ }
48
+ function checkAuth(req) {
49
+ if (!API_KEY)
50
+ return true; // Auth disabled when env var is not set
51
+ const header = req.headers['authorization'];
52
+ if (typeof header !== 'string')
53
+ return false;
54
+ const match = header.match(/^Bearer\s+(.+)$/i);
55
+ return match !== null && match[1] === API_KEY;
56
+ }
57
+ function setCorsHeaders(res) {
58
+ res.setHeader('Access-Control-Allow-Origin', '*');
59
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');
60
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
61
+ }
62
+ const httpServer = createServer(async (req, res) => {
63
+ setCorsHeaders(res);
64
+ // CORS preflight
65
+ if (req.method === 'OPTIONS') {
66
+ res.writeHead(204, { 'Access-Control-Max-Age': '86400' });
67
+ res.end();
68
+ return;
69
+ }
70
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
71
+ // Health check — unauthenticated, useful for uptime probes
72
+ if (url.pathname === '/health' && req.method === 'GET') {
73
+ writeJson(res, 200, { status: 'ok' });
74
+ return;
75
+ }
76
+ if (url.pathname !== MCP_PATH) {
77
+ writeJson(res, 404, { error: 'Not found' });
78
+ return;
79
+ }
80
+ if (!checkAuth(req)) {
81
+ res.setHeader('WWW-Authenticate', 'Bearer');
82
+ writeJson(res, 401, { error: 'Unauthorized' });
83
+ return;
84
+ }
85
+ // Only POST is supported in stateless mode (no server-initiated streams).
86
+ if (req.method !== 'POST') {
87
+ jsonRpcError(res, 405, -32000, 'Method not allowed.');
88
+ return;
89
+ }
90
+ try {
91
+ const body = await readJsonBody(req);
92
+ const server = createServerInstance();
93
+ const transport = new StreamableHTTPServerTransport({}); // stateless
94
+ // SDK types widen onclose/onerror with `| undefined`, which trips
95
+ // exactOptionalPropertyTypes against the stricter Transport interface.
96
+ await server.connect(transport);
97
+ res.on('close', () => {
98
+ void transport.close();
99
+ void server.close();
100
+ });
101
+ await transport.handleRequest(req, res, body);
102
+ }
103
+ catch (err) {
104
+ log(`error handling request: ${err.message}`);
105
+ if (!res.headersSent) {
106
+ jsonRpcError(res, 500, -32603, 'Internal server error');
107
+ }
108
+ }
109
+ });
110
+ httpServer.listen(PORT, HOST, () => {
111
+ log(`listening on http://${HOST}:${PORT}${MCP_PATH}`);
112
+ if (!API_KEY) {
113
+ log('WARNING: LUCENT_API_KEY is not set — the server is unauthenticated.');
114
+ }
115
+ if (HOST === '0.0.0.0' || HOST === '::') {
116
+ log(`WARNING: bound to ${HOST}. Set LUCENT_API_KEY before exposing publicly.`);
117
+ }
118
+ });
119
+ function shutdown() {
120
+ log('shutting down...');
121
+ httpServer.close(() => process.exit(0));
122
+ // Force-exit if graceful close hangs
123
+ setTimeout(() => process.exit(1), 5000).unref();
124
+ }
125
+ process.on('SIGINT', shutdown);
126
+ process.on('SIGTERM', shutdown);
@@ -1,11 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { z } from 'zod';
5
- import { ALL_MANIFESTS } from './registry.js';
6
- import { ALL_PATTERNS } from './pattern-registry.js';
7
- import { PALETTES, SHAPES, DENSITIES, SHADOWS, COMBINED, generatePresetConfig } from './presets.js';
8
- import { DESIGN_RULES, DESIGN_RULES_SUMMARY } from './design-rules.js';
4
+ import { registerTools, DESIGN_RULES_SUMMARY } from './tools.js';
9
5
  // ─── Auth stub ───────────────────────────────────────────────────────────────
10
6
  // LUCENT_API_KEY is reserved for the future paid tier.
11
7
  // When set, the server acknowledges it but does not yet enforce it.
@@ -13,55 +9,6 @@ const apiKey = process.env['LUCENT_API_KEY'];
13
9
  if (apiKey) {
14
10
  process.stderr.write('[lucent-mcp] Auth mode active (LUCENT_API_KEY is set).\n');
15
11
  }
16
- // ─── Helpers ─────────────────────────────────────────────────────────────────
17
- function findManifest(nameOrId) {
18
- const q = nameOrId.trim().toLowerCase();
19
- return ALL_MANIFESTS.find((m) => m.id.toLowerCase() === q || m.name.toLowerCase() === q);
20
- }
21
- function scoreManifest(m, query) {
22
- const q = query.toLowerCase();
23
- let score = 0;
24
- if (m.name.toLowerCase().includes(q))
25
- score += 10;
26
- if (m.id.toLowerCase().includes(q))
27
- score += 8;
28
- if (m.tier.toLowerCase().includes(q))
29
- score += 5;
30
- if (m.description.toLowerCase().includes(q))
31
- score += 4;
32
- if (m.designIntent.toLowerCase().includes(q))
33
- score += 3;
34
- for (const p of m.props) {
35
- if (p.name.toLowerCase().includes(q))
36
- score += 2;
37
- if (p.description.toLowerCase().includes(q))
38
- score += 1;
39
- }
40
- return score;
41
- }
42
- function findPattern(nameOrId) {
43
- const q = nameOrId.trim().toLowerCase();
44
- return ALL_PATTERNS.find((r) => r.id.toLowerCase() === q || r.name.toLowerCase() === q);
45
- }
46
- function scorePattern(r, query) {
47
- const q = query.toLowerCase();
48
- let score = 0;
49
- if (r.name.toLowerCase().includes(q))
50
- score += 10;
51
- if (r.id.toLowerCase().includes(q))
52
- score += 8;
53
- if (r.category.toLowerCase().includes(q))
54
- score += 5;
55
- if (r.description.toLowerCase().includes(q))
56
- score += 4;
57
- if (r.designNotes.toLowerCase().includes(q))
58
- score += 3;
59
- for (const c of r.components) {
60
- if (c.toLowerCase().includes(q))
61
- score += 2;
62
- }
63
- return score;
64
- }
65
12
  // ─── MCP Server ───────────────────────────────────────────────────────────────
66
13
  const server = new McpServer({
67
14
  name: 'lucent-mcp',
@@ -69,245 +16,7 @@ const server = new McpServer({
69
16
  }, {
70
17
  instructions: DESIGN_RULES_SUMMARY,
71
18
  });
72
- // Tool: list_components
73
- server.tool('list_components', 'Lists all available Lucent UI components with their name, tier (atom/molecule), and one-line description.', {}, async () => {
74
- const components = ALL_MANIFESTS.map((m) => ({
75
- id: m.id,
76
- name: m.name,
77
- tier: m.tier,
78
- description: m.description,
79
- }));
80
- return {
81
- content: [
82
- {
83
- type: 'text',
84
- text: JSON.stringify({ components }, null, 2),
85
- },
86
- ],
87
- };
88
- });
89
- // Tool: get_component_manifest
90
- server.tool('get_component_manifest', 'Returns the full manifest JSON for a Lucent UI component, including props, usage examples, design intent, and accessibility notes.', { componentName: z.string().describe('Component name or id, e.g. "Button" or "form-field"') }, async ({ componentName }) => {
91
- const manifest = findManifest(componentName);
92
- if (!manifest) {
93
- return {
94
- content: [
95
- {
96
- type: 'text',
97
- text: JSON.stringify({
98
- error: `Component "${componentName}" not found.`,
99
- available: ALL_MANIFESTS.map((m) => m.name),
100
- }),
101
- },
102
- ],
103
- isError: true,
104
- };
105
- }
106
- return {
107
- content: [
108
- {
109
- type: 'text',
110
- text: JSON.stringify(manifest, null, 2),
111
- },
112
- ],
113
- };
114
- });
115
- // Tool: search_components
116
- server.tool('search_components', 'Searches Lucent UI components and composition patterns by description or concept. Returns matching components and patterns ranked by relevance.', { query: z.string().describe('Natural language or keyword query, e.g. "loading indicator", "form validation", or "profile card"') }, async ({ query }) => {
117
- const componentResults = ALL_MANIFESTS
118
- .map((m) => ({ manifest: m, score: scoreManifest(m, query) }))
119
- .filter(({ score }) => score > 0)
120
- .sort((a, b) => b.score - a.score)
121
- .map(({ manifest, score }) => ({
122
- id: manifest.id,
123
- name: manifest.name,
124
- tier: manifest.tier,
125
- description: manifest.description,
126
- score,
127
- }));
128
- const patternResults = ALL_PATTERNS
129
- .map((r) => ({ pattern: r, score: scorePattern(r, query) }))
130
- .filter(({ score }) => score > 0)
131
- .sort((a, b) => b.score - a.score)
132
- .map(({ pattern, score }) => ({
133
- id: pattern.id,
134
- name: pattern.name,
135
- category: pattern.category,
136
- description: pattern.description,
137
- score,
138
- }));
139
- return {
140
- content: [
141
- {
142
- type: 'text',
143
- text: JSON.stringify({ query, components: componentResults, patterns: patternResults }, null, 2),
144
- },
145
- ],
146
- };
147
- });
148
- // Tool: get_composition_pattern
149
- server.tool('get_composition_pattern', 'Returns a full composition pattern with structure tree, working JSX code, variants, and design notes. Query by pattern name/id or by category to get all patterns in that category.', {
150
- name: z.string().optional().describe('Pattern name or id, e.g. "Profile Card" or "settings-panel"'),
151
- category: z.string().optional().describe('Pattern category: "card", "form", "nav", "dashboard", "settings", or "action"'),
152
- }, async ({ name, category }) => {
153
- if (name) {
154
- const pattern = findPattern(name);
155
- if (!pattern) {
156
- return {
157
- content: [
158
- {
159
- type: 'text',
160
- text: JSON.stringify({
161
- error: `Pattern "${name}" not found.`,
162
- available: ALL_PATTERNS.map((r) => ({ id: r.id, name: r.name, category: r.category })),
163
- }),
164
- },
165
- ],
166
- isError: true,
167
- };
168
- }
169
- return {
170
- content: [
171
- {
172
- type: 'text',
173
- text: JSON.stringify(pattern, null, 2),
174
- },
175
- ],
176
- };
177
- }
178
- if (category) {
179
- const cat = category.trim().toLowerCase();
180
- const patterns = ALL_PATTERNS.filter((r) => r.category === cat);
181
- if (patterns.length === 0) {
182
- return {
183
- content: [
184
- {
185
- type: 'text',
186
- text: JSON.stringify({
187
- error: `No patterns found in category "${category}".`,
188
- availableCategories: [...new Set(ALL_PATTERNS.map((r) => r.category))],
189
- }),
190
- },
191
- ],
192
- isError: true,
193
- };
194
- }
195
- return {
196
- content: [
197
- {
198
- type: 'text',
199
- text: JSON.stringify({ category: cat, patterns }, null, 2),
200
- },
201
- ],
202
- };
203
- }
204
- // No filter — return all patterns
205
- return {
206
- content: [
207
- {
208
- type: 'text',
209
- text: JSON.stringify({
210
- patterns: ALL_PATTERNS.map((r) => ({
211
- id: r.id,
212
- name: r.name,
213
- category: r.category,
214
- description: r.description,
215
- components: r.components,
216
- })),
217
- }, null, 2),
218
- },
219
- ],
220
- };
221
- });
222
- // Tool: list_presets
223
- server.tool('list_presets', 'Lists all available Lucent UI design presets. Returns combined presets (modern, enterprise, playful) and individual dimensions (palettes, shapes, densities, shadows) that can be mixed and matched.', {}, async () => {
224
- return {
225
- content: [
226
- {
227
- type: 'text',
228
- text: JSON.stringify({
229
- combined: COMBINED,
230
- palettes: PALETTES,
231
- shapes: SHAPES,
232
- densities: DENSITIES,
233
- shadows: SHADOWS,
234
- }, null, 2),
235
- },
236
- ],
237
- };
238
- });
239
- // Tool: get_preset_config
240
- server.tool('get_preset_config', 'Returns the LucentProvider configuration code for a given preset selection. Pass a combined preset name OR individual dimension names to get a ready-to-use config file and provider snippet.', {
241
- preset: z.string().optional().describe('Combined preset name: "modern", "enterprise", or "playful"'),
242
- palette: z.string().optional().describe('Palette name: "default", "brand", "indigo", "emerald", "rose", or "ocean"'),
243
- shape: z.string().optional().describe('Shape name: "sharp", "rounded", or "pill"'),
244
- density: z.string().optional().describe('Density name: "compact", "default", or "spacious"'),
245
- shadow: z.string().optional().describe('Shadow name: "flat", "subtle", or "elevated"'),
246
- }, async ({ preset, palette, shape, density, shadow }) => {
247
- const result = generatePresetConfig({ preset, palette, shape, density, shadow });
248
- if ('error' in result) {
249
- return {
250
- content: [{ type: 'text', text: JSON.stringify({ error: result.error }) }],
251
- isError: true,
252
- };
253
- }
254
- return {
255
- content: [
256
- {
257
- type: 'text',
258
- text: JSON.stringify({
259
- configFile: result.configFile,
260
- providerSnippet: result.providerSnippet,
261
- }, null, 2),
262
- },
263
- ],
264
- };
265
- });
266
- // Tool: get_design_rules
267
- server.tool('get_design_rules', 'Returns Lucent UI design rules for spacing, typography, button pairing, layout patterns, color usage, and density. These rules ensure AI-generated layouts are aesthetically consistent. Query a specific section or get all rules.', {
268
- section: z
269
- .string()
270
- .optional()
271
- .describe('Optional section id: "spacing", "typography", "buttons", "layout", "color", or "density". Omit to get all rules.'),
272
- }, async ({ section }) => {
273
- if (section) {
274
- const s = section.trim().toLowerCase();
275
- const rule = DESIGN_RULES.find((r) => r.id === s);
276
- if (!rule) {
277
- return {
278
- content: [
279
- {
280
- type: 'text',
281
- text: JSON.stringify({
282
- error: `Section "${section}" not found.`,
283
- availableSections: DESIGN_RULES.map((r) => ({
284
- id: r.id,
285
- title: r.title,
286
- })),
287
- }),
288
- },
289
- ],
290
- isError: true,
291
- };
292
- }
293
- return {
294
- content: [
295
- {
296
- type: 'text',
297
- text: `## ${rule.title}\n\n${rule.body}`,
298
- },
299
- ],
300
- };
301
- }
302
- return {
303
- content: [
304
- {
305
- type: 'text',
306
- text: DESIGN_RULES_SUMMARY,
307
- },
308
- ],
309
- };
310
- });
19
+ registerTools(server);
311
20
  // ─── Start ────────────────────────────────────────────────────────────────────
312
21
  const transport = new StdioServerTransport();
313
22
  await server.connect(transport);
@@ -0,0 +1,66 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Structured logging for MCP tool calls.
4
+ *
5
+ * One JSON line per call is written to stderr (greppable, parseable by log
6
+ * shippers). Set `LUCENT_MCP_QUIET=1` to disable all logging.
7
+ *
8
+ * When `LUCENT_API_KEY` is set, a short hash prefix of the key is included
9
+ * in log entries for usage analytics. The raw key is never logged.
10
+ */
11
+ const QUIET = process.env['LUCENT_MCP_QUIET'] === '1';
12
+ /**
13
+ * Returns the first 8 hex chars of sha256(key). Short enough to stay readable
14
+ * in logs, safe to leak (pre-image resistant), and unique enough to distinguish
15
+ * customers once multi-key auth lands (see issue #15).
16
+ */
17
+ function hashKeyPrefix(key) {
18
+ return createHash('sha256').update(key).digest('hex').slice(0, 8);
19
+ }
20
+ export function logToolCall(entry) {
21
+ if (QUIET)
22
+ return;
23
+ const apiKey = process.env['LUCENT_API_KEY'];
24
+ const line = JSON.stringify({
25
+ t: new Date().toISOString(),
26
+ tool: entry.tool,
27
+ params: entry.params,
28
+ durationMs: entry.durationMs,
29
+ ok: entry.ok,
30
+ ...(entry.error !== undefined && { error: entry.error }),
31
+ ...(apiKey !== undefined && { key: hashKeyPrefix(apiKey) }),
32
+ });
33
+ process.stderr.write(line + '\n');
34
+ }
35
+ /**
36
+ * Wraps a tool handler with timing + structured logging. The returned function
37
+ * has the same signature as the input, so it can be passed directly to
38
+ * `server.tool(...)` without any call-site changes.
39
+ */
40
+ export function withLogging(name, handler) {
41
+ const wrapped = async (...args) => {
42
+ const start = Date.now();
43
+ const params = args[0] ?? {};
44
+ try {
45
+ const result = await handler(...args);
46
+ logToolCall({
47
+ tool: name,
48
+ params,
49
+ durationMs: Date.now() - start,
50
+ ok: !result.isError,
51
+ });
52
+ return result;
53
+ }
54
+ catch (err) {
55
+ logToolCall({
56
+ tool: name,
57
+ params,
58
+ durationMs: Date.now() - start,
59
+ ok: false,
60
+ error: err instanceof Error ? err.message : String(err),
61
+ });
62
+ throw err;
63
+ }
64
+ };
65
+ return wrapped;
66
+ }
@@ -0,0 +1,298 @@
1
+ import { z } from 'zod';
2
+ import { ALL_MANIFESTS } from './registry.js';
3
+ import { ALL_PATTERNS } from './pattern-registry.js';
4
+ import { PALETTES, SHAPES, DENSITIES, SHADOWS, COMBINED, generatePresetConfig } from './presets.js';
5
+ import { DESIGN_RULES, DESIGN_RULES_SUMMARY } from './design-rules.js';
6
+ import { withLogging } from './logger.js';
7
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
8
+ function findManifest(nameOrId) {
9
+ const q = nameOrId.trim().toLowerCase();
10
+ return ALL_MANIFESTS.find((m) => m.id.toLowerCase() === q || m.name.toLowerCase() === q);
11
+ }
12
+ function scoreManifest(m, query) {
13
+ const q = query.toLowerCase();
14
+ let score = 0;
15
+ if (m.name.toLowerCase().includes(q))
16
+ score += 10;
17
+ if (m.id.toLowerCase().includes(q))
18
+ score += 8;
19
+ if (m.tier.toLowerCase().includes(q))
20
+ score += 5;
21
+ if (m.description.toLowerCase().includes(q))
22
+ score += 4;
23
+ if (m.designIntent.toLowerCase().includes(q))
24
+ score += 3;
25
+ for (const p of m.props) {
26
+ if (p.name.toLowerCase().includes(q))
27
+ score += 2;
28
+ if (p.description.toLowerCase().includes(q))
29
+ score += 1;
30
+ }
31
+ return score;
32
+ }
33
+ function findPattern(nameOrId) {
34
+ const q = nameOrId.trim().toLowerCase();
35
+ return ALL_PATTERNS.find((r) => r.id.toLowerCase() === q || r.name.toLowerCase() === q);
36
+ }
37
+ function scorePattern(r, query) {
38
+ const q = query.toLowerCase();
39
+ let score = 0;
40
+ if (r.name.toLowerCase().includes(q))
41
+ score += 10;
42
+ if (r.id.toLowerCase().includes(q))
43
+ score += 8;
44
+ if (r.category.toLowerCase().includes(q))
45
+ score += 5;
46
+ if (r.description.toLowerCase().includes(q))
47
+ score += 4;
48
+ if (r.designNotes.toLowerCase().includes(q))
49
+ score += 3;
50
+ for (const c of r.components) {
51
+ if (c.toLowerCase().includes(q))
52
+ score += 2;
53
+ }
54
+ return score;
55
+ }
56
+ // ─── Tool registrations ──────────────────────────────────────────────────────
57
+ export function registerTools(server) {
58
+ // Tool: list_components
59
+ server.tool('list_components', 'Lists all available Lucent UI components with their name, tier (atom/molecule), and one-line description.', {}, withLogging('list_components', async () => {
60
+ const components = ALL_MANIFESTS.map((m) => ({
61
+ id: m.id,
62
+ name: m.name,
63
+ tier: m.tier,
64
+ description: m.description,
65
+ }));
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: JSON.stringify({ components }, null, 2),
71
+ },
72
+ ],
73
+ };
74
+ }));
75
+ // Tool: get_component_manifest
76
+ server.tool('get_component_manifest', 'Returns the full manifest JSON for a Lucent UI component, including props, usage examples, design intent, and accessibility notes.', { componentName: z.string().describe('Component name or id, e.g. "Button" or "form-field"') }, withLogging('get_component_manifest', async ({ componentName }) => {
77
+ const manifest = findManifest(componentName);
78
+ if (!manifest) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: JSON.stringify({
84
+ error: `Component "${componentName}" not found.`,
85
+ available: ALL_MANIFESTS.map((m) => m.name),
86
+ }),
87
+ },
88
+ ],
89
+ isError: true,
90
+ };
91
+ }
92
+ return {
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: JSON.stringify(manifest, null, 2),
97
+ },
98
+ ],
99
+ };
100
+ }));
101
+ // Tool: search_components
102
+ server.tool('search_components', 'Searches Lucent UI components and composition patterns by description or concept. Returns matching components and patterns ranked by relevance.', { query: z.string().describe('Natural language or keyword query, e.g. "loading indicator", "form validation", or "profile card"') }, withLogging('search_components', async ({ query }) => {
103
+ const componentResults = ALL_MANIFESTS
104
+ .map((m) => ({ manifest: m, score: scoreManifest(m, query) }))
105
+ .filter(({ score }) => score > 0)
106
+ .sort((a, b) => b.score - a.score)
107
+ .map(({ manifest, score }) => ({
108
+ id: manifest.id,
109
+ name: manifest.name,
110
+ tier: manifest.tier,
111
+ description: manifest.description,
112
+ score,
113
+ }));
114
+ const patternResults = ALL_PATTERNS
115
+ .map((r) => ({ pattern: r, score: scorePattern(r, query) }))
116
+ .filter(({ score }) => score > 0)
117
+ .sort((a, b) => b.score - a.score)
118
+ .map(({ pattern, score }) => ({
119
+ id: pattern.id,
120
+ name: pattern.name,
121
+ category: pattern.category,
122
+ description: pattern.description,
123
+ score,
124
+ }));
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: JSON.stringify({ query, components: componentResults, patterns: patternResults }, null, 2),
130
+ },
131
+ ],
132
+ };
133
+ }));
134
+ // Tool: get_composition_pattern
135
+ server.tool('get_composition_pattern', 'Returns a full composition pattern with structure tree, working JSX code, variants, and design notes. Query by pattern name/id or by category to get all patterns in that category.', {
136
+ name: z.string().optional().describe('Pattern name or id, e.g. "Profile Card" or "settings-panel"'),
137
+ category: z.string().optional().describe('Pattern category: "card", "form", "nav", "dashboard", "settings", or "action"'),
138
+ }, withLogging('get_composition_pattern', async ({ name, category }) => {
139
+ if (name) {
140
+ const pattern = findPattern(name);
141
+ if (!pattern) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: 'text',
146
+ text: JSON.stringify({
147
+ error: `Pattern "${name}" not found.`,
148
+ available: ALL_PATTERNS.map((r) => ({ id: r.id, name: r.name, category: r.category })),
149
+ }),
150
+ },
151
+ ],
152
+ isError: true,
153
+ };
154
+ }
155
+ return {
156
+ content: [
157
+ {
158
+ type: 'text',
159
+ text: JSON.stringify(pattern, null, 2),
160
+ },
161
+ ],
162
+ };
163
+ }
164
+ if (category) {
165
+ const cat = category.trim().toLowerCase();
166
+ const patterns = ALL_PATTERNS.filter((r) => r.category === cat);
167
+ if (patterns.length === 0) {
168
+ return {
169
+ content: [
170
+ {
171
+ type: 'text',
172
+ text: JSON.stringify({
173
+ error: `No patterns found in category "${category}".`,
174
+ availableCategories: [...new Set(ALL_PATTERNS.map((r) => r.category))],
175
+ }),
176
+ },
177
+ ],
178
+ isError: true,
179
+ };
180
+ }
181
+ return {
182
+ content: [
183
+ {
184
+ type: 'text',
185
+ text: JSON.stringify({ category: cat, patterns }, null, 2),
186
+ },
187
+ ],
188
+ };
189
+ }
190
+ // No filter — return all patterns
191
+ return {
192
+ content: [
193
+ {
194
+ type: 'text',
195
+ text: JSON.stringify({
196
+ patterns: ALL_PATTERNS.map((r) => ({
197
+ id: r.id,
198
+ name: r.name,
199
+ category: r.category,
200
+ description: r.description,
201
+ components: r.components,
202
+ })),
203
+ }, null, 2),
204
+ },
205
+ ],
206
+ };
207
+ }));
208
+ // Tool: list_presets
209
+ server.tool('list_presets', 'Lists all available Lucent UI design presets. Returns combined presets (modern, enterprise, playful) and individual dimensions (palettes, shapes, densities, shadows) that can be mixed and matched.', {}, withLogging('list_presets', async () => {
210
+ return {
211
+ content: [
212
+ {
213
+ type: 'text',
214
+ text: JSON.stringify({
215
+ combined: COMBINED,
216
+ palettes: PALETTES,
217
+ shapes: SHAPES,
218
+ densities: DENSITIES,
219
+ shadows: SHADOWS,
220
+ }, null, 2),
221
+ },
222
+ ],
223
+ };
224
+ }));
225
+ // Tool: get_preset_config
226
+ server.tool('get_preset_config', 'Returns the LucentProvider configuration code for a given preset selection. Pass a combined preset name OR individual dimension names to get a ready-to-use config file and provider snippet.', {
227
+ preset: z.string().optional().describe('Combined preset name: "modern", "enterprise", or "playful"'),
228
+ palette: z.string().optional().describe('Palette name: "default", "brand", "indigo", "emerald", "rose", or "ocean"'),
229
+ shape: z.string().optional().describe('Shape name: "sharp", "rounded", or "pill"'),
230
+ density: z.string().optional().describe('Density name: "compact", "default", or "spacious"'),
231
+ shadow: z.string().optional().describe('Shadow name: "flat", "subtle", or "elevated"'),
232
+ }, withLogging('get_preset_config', async ({ preset, palette, shape, density, shadow }) => {
233
+ const result = generatePresetConfig({ preset, palette, shape, density, shadow });
234
+ if ('error' in result) {
235
+ return {
236
+ content: [{ type: 'text', text: JSON.stringify({ error: result.error }) }],
237
+ isError: true,
238
+ };
239
+ }
240
+ return {
241
+ content: [
242
+ {
243
+ type: 'text',
244
+ text: JSON.stringify({
245
+ configFile: result.configFile,
246
+ providerSnippet: result.providerSnippet,
247
+ }, null, 2),
248
+ },
249
+ ],
250
+ };
251
+ }));
252
+ // Tool: get_design_rules
253
+ server.tool('get_design_rules', 'Returns Lucent UI design rules for spacing, typography, button pairing, layout patterns, color usage, and density. These rules ensure AI-generated layouts are aesthetically consistent. Query a specific section or get all rules.', {
254
+ section: z
255
+ .string()
256
+ .optional()
257
+ .describe('Optional section id: "spacing", "typography", "buttons", "layout", "color", or "density". Omit to get all rules.'),
258
+ }, withLogging('get_design_rules', async ({ section }) => {
259
+ if (section) {
260
+ const s = section.trim().toLowerCase();
261
+ const rule = DESIGN_RULES.find((r) => r.id === s);
262
+ if (!rule) {
263
+ return {
264
+ content: [
265
+ {
266
+ type: 'text',
267
+ text: JSON.stringify({
268
+ error: `Section "${section}" not found.`,
269
+ availableSections: DESIGN_RULES.map((r) => ({
270
+ id: r.id,
271
+ title: r.title,
272
+ })),
273
+ }),
274
+ },
275
+ ],
276
+ isError: true,
277
+ };
278
+ }
279
+ return {
280
+ content: [
281
+ {
282
+ type: 'text',
283
+ text: `## ${rule.title}\n\n${rule.body}`,
284
+ },
285
+ ],
286
+ };
287
+ }
288
+ return {
289
+ content: [
290
+ {
291
+ type: 'text',
292
+ text: DESIGN_RULES_SUMMARY,
293
+ },
294
+ ],
295
+ };
296
+ }));
297
+ }
298
+ export { DESIGN_RULES_SUMMARY };
@@ -10,7 +10,10 @@ export const COMPONENT_MANIFEST = {
10
10
  'are not needed — props tables, changelog entries, comparison grids, reference docs. ' +
11
11
  'The compound API (Table.Head, Table.Body, Table.Row, Table.Cell) maps directly to ' +
12
12
  'semantic HTML so screen readers get the full table structure. ' +
13
- 'Horizontal overflow is handled automatically by a scroll wrapper.',
13
+ 'Horizontal overflow is handled automatically by a scroll wrapper. ' +
14
+ 'The wrapper paints var(--lucent-surface) as its background so the table always sits on a ' +
15
+ 'solid panel, regardless of parent page color; thead/tfoot/striped tints are translucent ' +
16
+ 'color-mix overlays on top of that surface so they adapt to both light and dark modes.',
14
17
  props: [
15
18
  {
16
19
  name: 'striped',
@@ -14,7 +14,9 @@ export const COMPONENT_MANIFEST = {
14
14
  'Filter → sort → paginate is the fixed pipeline order; any filter change resets the page to 0. ' +
15
15
  'Pagination is either controlled (page prop + onPageChange) or uncontrolled (internal state). ' +
16
16
  'A pageSize of 0 disables pagination entirely, useful when the parent manages windowing. ' +
17
- 'Row hover uses bg-subtle, not a border change, so the visual weight stays low for dense data views.',
17
+ 'Row hover uses bg-subtle, not a border change, so the visual weight stays low for dense data views. ' +
18
+ 'The table wrapper paints var(--lucent-surface) as its background so the component always sits on a solid panel, ' +
19
+ 'regardless of parent page color; header tints and row hover are translucent color-mix overlays on top of that surface.',
18
20
  props: [
19
21
  {
20
22
  name: 'columns',
@@ -7,6 +7,8 @@ export const COMPONENT_MANIFEST = {
7
7
  description: 'A drag-and-drop file upload zone with a file list, per-file progress bars, error display, and size/type validation.',
8
8
  designIntent: 'FileUpload separates concerns between the drop zone (entry point) and the file list (status display). ' +
9
9
  'The drop zone uses a dashed border and an upload arrow icon to communicate droppability without words. ' +
10
+ 'Its idle background and border are translucent color-mix overlays on top of var(--lucent-text-primary) (not hard-coded surface tokens), ' +
11
+ 'so the drop zone always reads as a visible step against whatever parent background it sits on — in any theme, in any palette. ' +
10
12
  'Progress is modelled as a field on UploadFile rather than as a callback so the parent controls upload logic — this component is purely presentational for the upload state. ' +
11
13
  'The progress bar turns success-green at 100% to give clear completion feedback. ' +
12
14
  'Errors are shown inline on each file row (not as a toast) so the user knows exactly which file failed and why. ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucent-ui",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "An AI-first React component library with machine-readable manifests.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,6 +22,7 @@
22
22
  "bin": {
23
23
  "lucent-ui": "./dist-cli/cli/entry.js",
24
24
  "lucent-mcp": "./dist-server/server/index.js",
25
+ "lucent-mcp-http": "./dist-server/server/http.js",
25
26
  "lucent-manifest": "./dist-cli/cli/index.js"
26
27
  },
27
28
  "sideEffects": false,