lucent-ui 0.2.0 → 0.3.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
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),g=require("react"),Q={primary:{background:"var(--lucent-accent-default)",color:"var(--lucent-text-on-accent)",border:"1px solid var(--lucent-accent-default)"},secondary:{background:"var(--lucent-surface-default)",color:"var(--lucent-text-primary)",border:"1px solid var(--lucent-border-default)"},ghost:{background:"transparent",color:"var(--lucent-text-primary)",border:"1px solid transparent"},danger:{background:"var(--lucent-danger-default)",color:"#ffffff",border:"1px solid var(--lucent-danger-default)"}},Z={sm:{height:"32px",padding:"0 var(--lucent-space-3)",fontSize:"var(--lucent-font-size-sm)"},md:{height:"38px",padding:"0 var(--lucent-space-4)",fontSize:"var(--lucent-font-size-md)"},lg:{height:"46px",padding:"0 var(--lucent-space-5)",fontSize:"var(--lucent-font-size-lg)"}},B=g.forwardRef(({variant:n="primary",size:t="md",loading:a=!1,fullWidth:r=!1,spread:o=!1,leftIcon:i,rightIcon:s,chevron:l=!1,disableHoverStyles:d=!1,children:p,disabled:f,style:m,...c},u)=>{const b=f??a;return e.jsxs("button",{ref:u,disabled:b,"aria-busy":a,style:{display:"inline-flex",alignItems:"center",justifyContent:o?"space-between":"center",gap:"var(--lucent-space-2)",fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-medium)",lineHeight:1,letterSpacing:"0.01em",borderRadius:"var(--lucent-radius-lg)",cursor:b?"not-allowed":"pointer",width:r?"100%":void 0,transition:"background var(--lucent-duration-fast) var(--lucent-easing-default), border-color var(--lucent-duration-fast) var(--lucent-easing-default), box-shadow var(--lucent-duration-fast) var(--lucent-easing-default), transform 80ms var(--lucent-easing-default)",whiteSpace:"nowrap",boxSizing:"border-box",outline:"none",margin:0,...Z[t],...Q[n],...m,...b&&{background:"var(--lucent-bg-muted)",color:"var(--lucent-text-disabled)",borderColor:"transparent"}},onMouseEnter:h=>{var x;!b&&!d&&ee(h.currentTarget,n),(x=c.onMouseEnter)==null||x.call(c,h)},onMouseLeave:h=>{var x;!b&&!d&&te(h.currentTarget,n),(x=c.onMouseLeave)==null||x.call(c,h)},onMouseDown:h=>{var x;b||(h.currentTarget.style.transform="scale(0.95)"),(x=c.onMouseDown)==null||x.call(c,h)},onMouseUp:h=>{var x;h.currentTarget.style.transform="",(x=c.onMouseUp)==null||x.call(c,h)},onFocus:h=>{var x;h.currentTarget.style.boxShadow="0 0 0 3px var(--lucent-accent-subtle)",(x=c.onFocus)==null||x.call(c,h)},onBlur:h=>{var x;h.currentTarget.style.boxShadow="",(x=c.onBlur)==null||x.call(c,h)},...c,children:[i,a?e.jsx(re,{}):p,!a&&s,!a&&l&&e.jsx(ae,{size:t})]})});B.displayName="Button";function ee(n,t){t==="primary"?(n.style.background="var(--lucent-accent-hover)",n.style.borderColor="var(--lucent-accent-hover)"):t==="secondary"?n.style.background="var(--lucent-bg-subtle)":t==="ghost"?n.style.background="var(--lucent-bg-muted)":t==="danger"&&(n.style.background="var(--lucent-danger-hover)",n.style.borderColor="var(--lucent-danger-hover)")}function te(n,t){t==="primary"?(n.style.background="var(--lucent-accent-default)",n.style.borderColor="var(--lucent-accent-default)"):t==="secondary"?n.style.background="var(--lucent-surface-default)":t==="ghost"?n.style.background="transparent":t==="danger"&&(n.style.background="var(--lucent-danger-default)",n.style.borderColor="var(--lucent-danger-default)")}const ne={sm:12,md:14,lg:16};function ae({size:n}){const t=ne[n];return e.jsx("svg",{width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":!0,style:{flexShrink:0,marginLeft:-2},children:e.jsx("polyline",{points:"6 9 12 15 18 9"})})}function re(){return e.jsxs("svg",{width:14,height:14,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round","aria-hidden":!0,style:{animation:"lucent-spin 0.7s linear infinite",flexShrink:0},children:[e.jsx("style",{children:"@keyframes lucent-spin { to { transform: rotate(360deg); } }"}),e.jsx("path",{d:"M12 2a10 10 0 0 1 10 10"})]})}const oe={id:"button",name:"Button",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A clickable control that triggers an action. The primary interactive primitive in Lucent UI.",designIntent:'Buttons communicate available actions. Variant conveys hierarchy: use "primary" for the single most important action in a view, "secondary" for supporting actions, "ghost" for low-emphasis actions in dense UIs, and "danger" exclusively for destructive or irreversible operations. Size should match surrounding content density — prefer "md" as the default and reserve "sm" for toolbars or tables.',props:[{name:"variant",type:"enum",required:!1,default:"primary",description:"Visual style conveying action hierarchy.",enumValues:["primary","secondary","ghost","danger"]},{name:"size",type:"enum",required:!1,default:"md",description:"Controls height and padding.",enumValues:["sm","md","lg"]},{name:"children",type:"ReactNode",required:!0,description:"Button label or content."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Prevents interaction and applies disabled styling."},{name:"loading",type:"boolean",required:!1,default:"false",description:"Shows a spinner and prevents interaction while an async action is in progress."},{name:"fullWidth",type:"boolean",required:!1,default:"false",description:"Stretches the button to fill its container width."},{name:"leftIcon",type:"ReactNode",required:!1,description:"Icon element rendered before the label."},{name:"rightIcon",type:"ReactNode",required:!1,description:"Icon element rendered after the label."},{name:"onClick",type:"function",required:!1,description:"Called when the button is clicked and not disabled or loading."},{name:"type",type:"enum",required:!1,default:"button",description:"Native button type attribute.",enumValues:["button","submit","reset"]}],usageExamples:[{title:"Primary action",code:'<Button variant="primary" onClick={handleSave}>Save changes</Button>'},{title:"Destructive action",code:'<Button variant="danger" onClick={handleDelete}>Delete account</Button>'},{title:"Loading state",code:'<Button variant="primary" loading={isSaving}>Save changes</Button>'},{title:"With icon",code:'<Button variant="secondary" leftIcon={<PlusIcon />}>Add member</Button>'},{title:"Ghost in toolbar",code:'<Button variant="ghost" size="sm">Edit</Button>'},{title:"Full-width submit",code:'<Button variant="primary" type="submit" fullWidth>Sign in</Button>'}],compositionGraph:[],accessibility:{role:"button",ariaAttributes:["aria-disabled","aria-busy"],keyboardInteractions:["Enter — activates the button","Space — activates the button"]}},E=g.forwardRef(({label:n,helperText:t,errorText:a,leftElement:r,rightElement:o,id:i,style:s,...l},d)=>{const p=i??`lucent-input-${Math.random().toString(36).slice(2,7)}`,f=!!a,m=!!l.disabled;return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",width:"100%"},children:[n&&e.jsx("label",{htmlFor:p,style:{fontSize:"var(--lucent-font-size-sm)",fontWeight:"var(--lucent-font-weight-medium)",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)"},children:n}),e.jsxs("div",{style:{position:"relative",display:"flex",alignItems:"center"},children:[r&&e.jsx("span",{style:{position:"absolute",left:"var(--lucent-space-3)",color:m?"var(--lucent-text-disabled)":"var(--lucent-text-secondary)",display:"flex",alignItems:"center",pointerEvents:"none"},children:r}),e.jsx("input",{ref:d,id:p,"aria-invalid":f,"aria-describedby":f?`${p}-error`:t?`${p}-helper`:void 0,style:{width:"100%",height:"40px",padding:`0 ${o?"var(--lucent-space-10)":"var(--lucent-space-3)"} 0 ${r?"var(--lucent-space-10)":"var(--lucent-space-3)"}`,fontSize:"var(--lucent-font-size-md)",fontFamily:"var(--lucent-font-family-base)",color:m?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",background:m?"var(--lucent-bg-muted)":"var(--lucent-surface-default)",border:`1px solid ${m?"transparent":f?"var(--lucent-danger-default)":"var(--lucent-border-default)"}`,cursor:m?"not-allowed":void 0,borderRadius:"var(--lucent-radius-lg)",outline:"none",boxSizing:"border-box",transition:"border-color var(--lucent-duration-fast) var(--lucent-easing-default)",...s},onMouseEnter:c=>{var u;!l.disabled&&c.currentTarget!==document.activeElement&&(c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-border-strong)"),(u=l.onMouseEnter)==null||u.call(l,c)},onMouseLeave:c=>{var u;!l.disabled&&c.currentTarget!==document.activeElement&&(c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-border-default)"),(u=l.onMouseLeave)==null||u.call(l,c)},onFocus:c=>{var u;c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-focus-ring)",c.currentTarget.style.boxShadow=`0 0 0 3px ${f?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`,(u=l.onFocus)==null||u.call(l,c)},onBlur:c=>{var u;c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-border-default)",c.currentTarget.style.boxShadow="none",(u=l.onBlur)==null||u.call(l,c)},...l}),o&&e.jsx("span",{style:{position:"absolute",right:"var(--lucent-space-3)",color:m?"var(--lucent-text-disabled)":"var(--lucent-text-secondary)",display:"flex",alignItems:"center"},children:o})]}),f&&e.jsx("span",{id:`${p}-error`,role:"alert",style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:a}),!f&&t&&e.jsx("span",{id:`${p}-helper`,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)"},children:t})]})});E.displayName="Input";const ie={id:"input",name:"Input",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A single-line text field with optional label, helper text, and error state.",designIntent:"Always pair with a visible label — never rely on placeholder text alone as it disappears on input and is inaccessible. Use errorText (not helperText) to surface validation failures; the component applies danger styling automatically. leftElement and rightElement accept icons or small controls (e.g. currency symbol, clear button).",props:[{name:"type",type:"enum",required:!1,default:"text",description:"HTML input type.",enumValues:["text","number","password","email","tel","url","search"]},{name:"label",type:"string",required:!1,description:"Visible label rendered above the input."},{name:"helperText",type:"string",required:!1,description:"Supplementary hint shown below the input."},{name:"errorText",type:"string",required:!1,description:"Validation error message. When set, input renders in error state."},{name:"leftElement",type:"ReactNode",required:!1,description:"Icon or adornment rendered inside the left edge."},{name:"rightElement",type:"ReactNode",required:!1,description:"Icon or adornment rendered inside the right edge."},{name:"placeholder",type:"string",required:!1,description:"Placeholder text. Use as a hint, not a label."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the input."},{name:"value",type:"string",required:!1,description:"Controlled value."},{name:"onChange",type:"function",required:!1,description:"Change handler."}],usageExamples:[{title:"Basic",code:'<Input label="Email" type="email" placeholder="you@example.com" />'},{title:"With helper text",code:'<Input label="Username" helperText="3–20 characters, letters and numbers only" />'},{title:"Error state",code:'<Input label="Password" type="password" value={value} errorText="Must be at least 8 characters" />'},{title:"With icon",code:'<Input label="Search" leftElement={<SearchIcon />} placeholder="Search…" />'}],compositionGraph:[],accessibility:{role:"textbox",ariaAttributes:["aria-invalid","aria-describedby","aria-label"],keyboardInteractions:["Tab — focuses the input"]}},$=g.forwardRef(({label:n,helperText:t,errorText:a,autoResize:r=!1,maxLength:o,showCount:i=!1,id:s,value:l,onChange:d,style:p,...f},m)=>{const c=g.useRef(null),u=m??c,b=s??`lucent-textarea-${Math.random().toString(36).slice(2,7)}`,h=!!a,x=typeof l=="string"?l.length:0;return g.useEffect(()=>{if(!r)return;const w=u.current;w&&(w.style.height="auto",w.style.height=`${w.scrollHeight}px`)},[l,r,u]),e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",width:"100%"},children:[n&&e.jsx("label",{htmlFor:b,style:{fontSize:"var(--lucent-font-size-sm)",fontWeight:"var(--lucent-font-weight-medium)",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)"},children:n}),e.jsx("textarea",{ref:u,id:b,maxLength:o,value:l,onChange:d,"aria-invalid":h,"aria-describedby":h?`${b}-error`:t?`${b}-helper`:void 0,style:{width:"100%",minHeight:"100px",padding:"var(--lucent-space-3)",fontSize:"var(--lucent-font-size-md)",fontFamily:"var(--lucent-font-family-base)",color:"var(--lucent-text-primary)",background:"var(--lucent-surface-default)",border:`1px solid ${h?"var(--lucent-danger-default)":"var(--lucent-border-default)"}`,borderRadius:"var(--lucent-radius-md)",outline:"none",resize:r?"none":"vertical",boxSizing:"border-box",lineHeight:"var(--lucent-line-height-base)",transition:"border-color var(--lucent-duration-fast) var(--lucent-easing-default)",...p},onFocus:w=>{var v;w.currentTarget.style.borderColor=h?"var(--lucent-danger-default)":"var(--lucent-focus-ring)",w.currentTarget.style.boxShadow=`0 0 0 3px ${h?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`,(v=f.onFocus)==null||v.call(f,w)},onBlur:w=>{var v;w.currentTarget.style.borderColor=h?"var(--lucent-danger-default)":"var(--lucent-border-default)",w.currentTarget.style.boxShadow="none",(v=f.onBlur)==null||v.call(f,w)},...f}),e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start"},children:[e.jsxs("div",{children:[h&&e.jsx("span",{id:`${b}-error`,role:"alert",style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:a}),!h&&t&&e.jsx("span",{id:`${b}-helper`,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)"},children:t})]}),(i||o)&&e.jsxs("span",{style:{fontSize:"var(--lucent-font-size-xs)",color:o&&x>=o?"var(--lucent-danger-text)":"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-mono)",flexShrink:0,marginLeft:"var(--lucent-space-2)"},children:[x,o?`/${o}`:""]})]})]})});$.displayName="Textarea";const se={id:"textarea",name:"Textarea",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A multi-line text input with optional auto-resize and character count.",designIntent:"Use autoResize for open-ended fields (bio, description) where content length is unpredictable. Use maxLength + showCount for fields with hard limits (tweet-style). Behaves identically to Input for label/helper/error patterns.",props:[{name:"label",type:"string",required:!1,description:"Visible label above the textarea."},{name:"helperText",type:"string",required:!1,description:"Hint text shown below."},{name:"errorText",type:"string",required:!1,description:"Validation error. Triggers error styling."},{name:"autoResize",type:"boolean",required:!1,default:"false",description:"Grows with content, disables manual resize handle."},{name:"maxLength",type:"number",required:!1,description:"Character limit. Displays counter when set."},{name:"showCount",type:"boolean",required:!1,default:"false",description:"Always show character counter even without maxLength."},{name:"value",type:"string",required:!1,description:"Controlled value."},{name:"onChange",type:"function",required:!1,description:"Change handler."},{name:"placeholder",type:"string",required:!1,description:"Placeholder text."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the textarea."}],usageExamples:[{title:"Basic",code:'<Textarea label="Bio" placeholder="Tell us about yourself…" />'},{title:"Auto-resize",code:'<Textarea label="Description" autoResize value={value} onChange={e => setValue(e.target.value)} />'},{title:"With character count",code:'<Textarea label="Tweet" maxLength={280} showCount value={value} onChange={e => setValue(e.target.value)} />'},{title:"Error state",code:'<Textarea label="Notes" errorText="Required" value="" />'}],compositionGraph:[],accessibility:{role:"textbox",ariaAttributes:["aria-multiline","aria-invalid","aria-describedby"],keyboardInteractions:["Tab — focuses the textarea"]}},le={neutral:{bg:"var(--lucent-bg-muted)",color:"var(--lucent-text-secondary)",border:"var(--lucent-border-default)"},accent:{bg:"var(--lucent-accent-default)",color:"var(--lucent-text-on-accent)",border:"var(--lucent-accent-default)"},success:{bg:"var(--lucent-success-subtle)",color:"var(--lucent-success-text)",border:"var(--lucent-success-subtle)"},warning:{bg:"var(--lucent-warning-subtle)",color:"var(--lucent-warning-text)",border:"var(--lucent-warning-subtle)"},danger:{bg:"var(--lucent-danger-subtle)",color:"var(--lucent-danger-text)",border:"var(--lucent-danger-subtle)"},info:{bg:"var(--lucent-info-subtle)",color:"var(--lucent-info-text)",border:"var(--lucent-info-subtle)"}},ce={sm:{fontSize:"var(--lucent-font-size-xs)",padding:"0 var(--lucent-space-2)",height:"18px"},md:{fontSize:"var(--lucent-font-size-sm)",padding:"0 var(--lucent-space-2)",height:"22px"}};function de({variant:n="neutral",size:t="md",dot:a=!1,children:r,style:o}){const i=le[n],s=ce[t];return e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-1)",height:s.height,padding:s.padding,fontSize:s.fontSize,fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-medium)",lineHeight:1,borderRadius:"var(--lucent-radius-full)",background:i.bg,color:i.color,border:`1px solid ${i.border}`,whiteSpace:"nowrap",boxSizing:"border-box",...o},children:[a&&e.jsx("span",{style:{width:6,height:6,borderRadius:"var(--lucent-radius-full)",background:"currentColor",flexShrink:0}}),r]})}const ue={id:"badge",name:"Badge",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A small inline label for status, count, or category.",designIntent:'Badges communicate status or category at a glance. Match variant to semantic meaning — never use "danger" for non-critical states or "success" for neutral counts. Use dot=true when a single colour indicator is enough context (e.g. online status). Keep badge text short: 1–3 words maximum.',props:[{name:"variant",type:"enum",required:!1,default:"neutral",description:"Colour scheme conveying semantic meaning.",enumValues:["neutral","success","warning","danger","info","accent"]},{name:"size",type:"enum",required:!1,default:"md",description:"Controls height and font size.",enumValues:["sm","md"]},{name:"dot",type:"boolean",required:!1,default:"false",description:"Prepends a coloured dot indicator."},{name:"children",type:"ReactNode",required:!0,description:"Badge label."}],usageExamples:[{title:"Status",code:'<Badge variant="success" dot>Active</Badge>'},{title:"Count",code:'<Badge variant="danger">12</Badge>'},{title:"Category",code:'<Badge variant="info">Beta</Badge>'},{title:"Neutral tag",code:"<Badge>Draft</Badge>"}],compositionGraph:[],accessibility:{role:"status",notes:"Use aria-label on the parent element when badge meaning depends on context."}},pe={xs:24,sm:32,md:40,lg:56,xl:80},fe={xs:"var(--lucent-font-size-xs)",sm:"var(--lucent-font-size-xs)",md:"var(--lucent-font-size-sm)",lg:"var(--lucent-font-size-lg)",xl:"var(--lucent-font-size-xl)"};function me(n,t){var r,o,i;if(t)return t.slice(0,2).toUpperCase();const a=n.trim().split(/\s+/);return a.length===1?(((r=a[0])==null?void 0:r[0])??"").toUpperCase():((((o=a[0])==null?void 0:o[0])??"")+(((i=a[a.length-1])==null?void 0:i[0])??"")).toUpperCase()}function he({src:n,alt:t,size:a="md",initials:r,style:o,...i}){const s=pe[a],l=me(t,r),d={width:s,height:s,borderRadius:"var(--lucent-radius-full)",flexShrink:0,display:"inline-flex",alignItems:"center",justifyContent:"center",overflow:"hidden",boxSizing:"border-box",userSelect:"none",...o};return n?e.jsx("img",{src:n,alt:t,width:s,height:s,style:{...d,objectFit:"cover"},...i}):e.jsx("span",{role:"img","aria-label":t,style:{...d,background:"var(--lucent-accent-default)",color:"var(--lucent-text-on-accent)",fontSize:fe[a],fontWeight:"var(--lucent-font-weight-semibold)",fontFamily:"var(--lucent-font-family-base)"},children:l})}const ge={id:"avatar",name:"Avatar",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A circular user image with initials fallback.",designIntent:"Always provide alt for accessibility — it is used to derive initials automatically when src is absent or fails. Use initials prop to override auto-derived initials (e.g. for non-Latin names). Size xs/sm suit table rows and compact lists; md is the default for comment threads; lg/xl for profile headers.",props:[{name:"src",type:"string",required:!1,description:"Image URL. Falls back to initials if omitted or fails to load."},{name:"alt",type:"string",required:!0,description:"Alt text and source for auto-derived initials."},{name:"size",type:"enum",required:!1,default:"md",description:"Diameter of the avatar.",enumValues:["xs","sm","md","lg","xl"]},{name:"initials",type:"string",required:!1,description:"Override auto-derived initials (max 2 characters)."}],usageExamples:[{title:"With image",code:'<Avatar src="/avatars/jane.jpg" alt="Jane Doe" />'},{title:"Initials fallback",code:'<Avatar alt="Jane Doe" />'},{title:"Large profile",code:'<Avatar src={user.avatar} alt={user.name} size="lg" />'},{title:"Custom initials",code:'<Avatar alt="张伟" initials="张" size="md" />'}],compositionGraph:[],accessibility:{role:"img",ariaAttributes:["aria-label"],notes:'When src is present, renders as <img> with alt. When showing initials, renders as <span role="img" aria-label>.'}},be={xs:12,sm:16,md:24,lg:36},ve={xs:2.5,sm:2.5,md:2,lg:2};function L({size:n="md",label:t="Loading…",color:a}){const r=be[n],o=ve[n];return e.jsxs("span",{role:"status","aria-label":t,style:{display:"inline-flex",alignItems:"center",justifyContent:"center"},children:[e.jsxs("svg",{width:r,height:r,viewBox:"0 0 24 24",fill:"none","aria-hidden":!0,style:{animation:"lucent-spin 0.7s linear infinite",color:a??"currentColor"},children:[e.jsx("style",{children:"@keyframes lucent-spin { to { transform: rotate(360deg); } }"}),e.jsx("circle",{cx:12,cy:12,r:10,stroke:"currentColor",strokeWidth:o,strokeOpacity:.2}),e.jsx("path",{d:"M12 2a10 10 0 0 1 10 10",stroke:"currentColor",strokeWidth:o,strokeLinecap:"round"})]}),e.jsx("span",{style:{position:"absolute",width:1,height:1,overflow:"hidden",clip:"rect(0,0,0,0)",whiteSpace:"nowrap"},children:t})]})}const ye={id:"spinner",name:"Spinner",tier:"atom",domain:"neutral",specVersion:"0.1",description:"An animated loading indicator for async operations.",designIntent:"Use Spinner for indeterminate loading states of short duration (< 3s). For full-page or skeleton-level loading, prefer Skeleton instead. The label prop is visually hidden but read by screen readers — always set it to a meaningful description of what is loading.",props:[{name:"size",type:"enum",required:!1,default:"md",description:"Spinner diameter.",enumValues:["xs","sm","md","lg"]},{name:"label",type:"string",required:!1,default:"Loading…",description:"Visually hidden accessible label."},{name:"color",type:"string",required:!1,description:"Override colour (CSS value). Defaults to currentColor."}],usageExamples:[{title:"Default",code:"<Spinner />"},{title:"Inside button",code:'<Button loading><Spinner size="sm" label="Saving…" /></Button>'},{title:"Full-page overlay",code:`<div style={{ display: 'grid', placeItems: 'center', minHeight: '100vh' }}><Spinner size="lg" label="Loading dashboard…" /></div>`}],compositionGraph:[],accessibility:{role:"status",ariaAttributes:["aria-label"],notes:'The visible SVG is aria-hidden. The label is conveyed via a visually-hidden span inside role="status".'}};function xe({orientation:n="horizontal",label:t,spacing:a="var(--lucent-space-4)",style:r}){return n==="vertical"?e.jsx("span",{role:"separator","aria-orientation":"vertical",style:{display:"inline-block",width:"1px",alignSelf:"stretch",background:"var(--lucent-border-default)",margin:`0 ${a}`,flexShrink:0,...r}}):t?e.jsxs("div",{role:"separator","aria-label":t,style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-3)",margin:`${a} 0`,...r},children:[e.jsx("span",{style:{flex:1,height:"1px",background:"var(--lucent-border-default)"}}),e.jsx("span",{style:{fontSize:"var(--lucent-font-size-xs)",fontFamily:"var(--lucent-font-family-base)",color:"var(--lucent-text-secondary)",whiteSpace:"nowrap",letterSpacing:"var(--lucent-letter-spacing-wide)",textTransform:"uppercase"},children:t}),e.jsx("span",{style:{flex:1,height:"1px",background:"var(--lucent-border-default)"}})]}):e.jsx("hr",{role:"separator",style:{border:"none",borderTop:"1px solid var(--lucent-border-default)",margin:`${a} 0`,width:"100%",...r}})}const we={id:"divider",name:"Divider",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A visual separator between content sections, horizontal or vertical.",designIntent:'Use horizontal Divider to separate sections in a layout. Use vertical Divider inline between sibling elements (e.g. nav links, toolbar buttons). Use the label prop for "OR" separators in auth flows or form sections — never use a plain text node next to a divider for this.',props:[{name:"orientation",type:"enum",required:!1,default:"horizontal",description:"Direction of the divider line.",enumValues:["horizontal","vertical"]},{name:"label",type:"string",required:!1,description:'Optional centered label (horizontal only). Common use: "OR", "AND", section titles.'},{name:"spacing",type:"string",required:!1,default:"var(--lucent-space-4)",description:"Margin on the axis perpendicular to the line."}],usageExamples:[{title:"Section separator",code:"<Divider />"},{title:"With label",code:'<Divider label="OR" />'},{title:"Vertical in nav",code:`<nav style={{ display: 'flex', alignItems: 'center' }}><a>Home</a><Divider orientation="vertical" /><a>About</a></nav>`}],compositionGraph:[],accessibility:{role:"separator",ariaAttributes:["aria-orientation","aria-label"]}},Se={sm:14,md:16},ke=`
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),g=require("react"),Q={primary:{background:"var(--lucent-accent-default)",color:"var(--lucent-text-on-accent)",border:"1px solid var(--lucent-accent-default)"},secondary:{background:"var(--lucent-surface-default)",color:"var(--lucent-text-primary)",border:"1px solid var(--lucent-border-default)"},ghost:{background:"transparent",color:"var(--lucent-text-primary)",border:"1px solid transparent"},danger:{background:"var(--lucent-danger-default)",color:"#ffffff",border:"1px solid var(--lucent-danger-default)"}},Z={sm:{height:"32px",padding:"0 var(--lucent-space-3)",fontSize:"var(--lucent-font-size-sm)"},md:{height:"38px",padding:"0 var(--lucent-space-4)",fontSize:"var(--lucent-font-size-md)"},lg:{height:"46px",padding:"0 var(--lucent-space-5)",fontSize:"var(--lucent-font-size-lg)"}},B=g.forwardRef(({variant:n="primary",size:t="md",loading:a=!1,fullWidth:r=!1,spread:o=!1,leftIcon:i,rightIcon:s,chevron:l=!1,disableHoverStyles:d=!1,children:p,disabled:f,style:m,...c},u)=>{const b=f??a;return e.jsxs("button",{ref:u,disabled:b,"aria-busy":a,style:{display:"inline-flex",alignItems:"center",justifyContent:o?"space-between":"center",gap:"var(--lucent-space-2)",fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-medium)",lineHeight:1,letterSpacing:"0.01em",borderRadius:"var(--lucent-radius-lg)",cursor:b?"not-allowed":"pointer",width:r?"100%":void 0,transition:"background var(--lucent-duration-fast) var(--lucent-easing-default), border-color var(--lucent-duration-fast) var(--lucent-easing-default), box-shadow var(--lucent-duration-fast) var(--lucent-easing-default), transform 80ms var(--lucent-easing-default)",whiteSpace:"nowrap",boxSizing:"border-box",outline:"none",margin:0,...Z[t],...Q[n],...m,...b&&{background:"var(--lucent-bg-muted)",color:"var(--lucent-text-disabled)",borderColor:"transparent"}},onMouseEnter:h=>{var x;!b&&!d&&ee(h.currentTarget,n),(x=c.onMouseEnter)==null||x.call(c,h)},onMouseLeave:h=>{var x;!b&&!d&&te(h.currentTarget,n),(x=c.onMouseLeave)==null||x.call(c,h)},onMouseDown:h=>{var x;b||(h.currentTarget.style.transform="scale(0.95)"),(x=c.onMouseDown)==null||x.call(c,h)},onMouseUp:h=>{var x;h.currentTarget.style.transform="",(x=c.onMouseUp)==null||x.call(c,h)},onFocus:h=>{var x;h.currentTarget.style.boxShadow="0 0 0 3px var(--lucent-accent-subtle)",(x=c.onFocus)==null||x.call(c,h)},onBlur:h=>{var x;h.currentTarget.style.boxShadow="",(x=c.onBlur)==null||x.call(c,h)},...c,children:[i,a?e.jsx(re,{}):p,!a&&s,!a&&l&&e.jsx(ae,{size:t})]})});B.displayName="Button";function ee(n,t){t==="primary"?(n.style.background="var(--lucent-accent-hover)",n.style.borderColor="var(--lucent-accent-hover)"):t==="secondary"?n.style.background="var(--lucent-bg-subtle)":t==="ghost"?n.style.background="var(--lucent-bg-muted)":t==="danger"&&(n.style.background="var(--lucent-danger-hover)",n.style.borderColor="var(--lucent-danger-hover)")}function te(n,t){t==="primary"?(n.style.background="var(--lucent-accent-default)",n.style.borderColor="var(--lucent-accent-default)"):t==="secondary"?n.style.background="var(--lucent-surface-default)":t==="ghost"?n.style.background="transparent":t==="danger"&&(n.style.background="var(--lucent-danger-default)",n.style.borderColor="var(--lucent-danger-default)")}const ne={sm:12,md:14,lg:16};function ae({size:n}){const t=ne[n];return e.jsx("svg",{width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":!0,style:{flexShrink:0,marginLeft:-2},children:e.jsx("polyline",{points:"6 9 12 15 18 9"})})}function re(){return e.jsxs("svg",{width:14,height:14,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round","aria-hidden":!0,style:{animation:"lucent-spin 0.7s linear infinite",flexShrink:0},children:[e.jsx("style",{children:"@keyframes lucent-spin { to { transform: rotate(360deg); } }"}),e.jsx("path",{d:"M12 2a10 10 0 0 1 10 10"})]})}const oe={id:"button",name:"Button",tier:"atom",domain:"neutral",specVersion:"1.0",description:"A clickable control that triggers an action. The primary interactive primitive in Lucent UI.",designIntent:'Buttons communicate available actions. Variant conveys hierarchy: use "primary" for the single most important action in a view, "secondary" for supporting actions, "ghost" for low-emphasis actions in dense UIs, and "danger" exclusively for destructive or irreversible operations. Size should match surrounding content density — prefer "md" as the default and reserve "sm" for toolbars or tables.',props:[{name:"variant",type:"enum",required:!1,default:"primary",description:"Visual style conveying action hierarchy.",enumValues:["primary","secondary","ghost","danger"]},{name:"size",type:"enum",required:!1,default:"md",description:"Controls height and padding.",enumValues:["sm","md","lg"]},{name:"children",type:"ReactNode",required:!0,description:"Button label or content."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Prevents interaction and applies disabled styling."},{name:"loading",type:"boolean",required:!1,default:"false",description:"Shows a spinner and prevents interaction while an async action is in progress."},{name:"fullWidth",type:"boolean",required:!1,default:"false",description:"Stretches the button to fill its container width."},{name:"leftIcon",type:"ReactNode",required:!1,description:"Icon element rendered before the label."},{name:"rightIcon",type:"ReactNode",required:!1,description:"Icon element rendered after the label."},{name:"onClick",type:"function",required:!1,description:"Called when the button is clicked and not disabled or loading."},{name:"type",type:"enum",required:!1,default:"button",description:"Native button type attribute.",enumValues:["button","submit","reset"]}],usageExamples:[{title:"Primary action",code:'<Button variant="primary" onClick={handleSave}>Save changes</Button>'},{title:"Destructive action",code:'<Button variant="danger" onClick={handleDelete}>Delete account</Button>'},{title:"Loading state",code:'<Button variant="primary" loading={isSaving}>Save changes</Button>'},{title:"With icon",code:'<Button variant="secondary" leftIcon={<PlusIcon />}>Add member</Button>'},{title:"Ghost in toolbar",code:'<Button variant="ghost" size="sm">Edit</Button>'},{title:"Full-width submit",code:'<Button variant="primary" type="submit" fullWidth>Sign in</Button>'}],compositionGraph:[],accessibility:{role:"button",ariaAttributes:["aria-disabled","aria-busy"],keyboardInteractions:["Enter — activates the button","Space — activates the button"]}},E=g.forwardRef(({label:n,helperText:t,errorText:a,leftElement:r,rightElement:o,id:i,style:s,...l},d)=>{const p=i??`lucent-input-${Math.random().toString(36).slice(2,7)}`,f=!!a,m=!!l.disabled;return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",width:"100%"},children:[n&&e.jsx("label",{htmlFor:p,style:{fontSize:"var(--lucent-font-size-sm)",fontWeight:"var(--lucent-font-weight-medium)",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)"},children:n}),e.jsxs("div",{style:{position:"relative",display:"flex",alignItems:"center"},children:[r&&e.jsx("span",{style:{position:"absolute",left:"var(--lucent-space-3)",color:m?"var(--lucent-text-disabled)":"var(--lucent-text-secondary)",display:"flex",alignItems:"center",pointerEvents:"none"},children:r}),e.jsx("input",{ref:d,id:p,"aria-invalid":f,"aria-describedby":f?`${p}-error`:t?`${p}-helper`:void 0,style:{width:"100%",height:"40px",padding:`0 ${o?"var(--lucent-space-10)":"var(--lucent-space-3)"} 0 ${r?"var(--lucent-space-10)":"var(--lucent-space-3)"}`,fontSize:"var(--lucent-font-size-md)",fontFamily:"var(--lucent-font-family-base)",color:m?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",background:m?"var(--lucent-bg-muted)":"var(--lucent-surface-default)",border:`1px solid ${m?"transparent":f?"var(--lucent-danger-default)":"var(--lucent-border-default)"}`,cursor:m?"not-allowed":void 0,borderRadius:"var(--lucent-radius-lg)",outline:"none",boxSizing:"border-box",transition:"border-color var(--lucent-duration-fast) var(--lucent-easing-default)",...s},onMouseEnter:c=>{var u;!l.disabled&&c.currentTarget!==document.activeElement&&(c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-border-strong)"),(u=l.onMouseEnter)==null||u.call(l,c)},onMouseLeave:c=>{var u;!l.disabled&&c.currentTarget!==document.activeElement&&(c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-border-default)"),(u=l.onMouseLeave)==null||u.call(l,c)},onFocus:c=>{var u;c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-focus-ring)",c.currentTarget.style.boxShadow=`0 0 0 3px ${f?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`,(u=l.onFocus)==null||u.call(l,c)},onBlur:c=>{var u;c.currentTarget.style.borderColor=f?"var(--lucent-danger-default)":"var(--lucent-border-default)",c.currentTarget.style.boxShadow="none",(u=l.onBlur)==null||u.call(l,c)},...l}),o&&e.jsx("span",{style:{position:"absolute",right:"var(--lucent-space-3)",color:m?"var(--lucent-text-disabled)":"var(--lucent-text-secondary)",display:"flex",alignItems:"center"},children:o})]}),f&&e.jsx("span",{id:`${p}-error`,role:"alert",style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:a}),!f&&t&&e.jsx("span",{id:`${p}-helper`,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)"},children:t})]})});E.displayName="Input";const ie={id:"input",name:"Input",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A single-line text field with optional label, helper text, and error state.",designIntent:"Always pair with a visible label — never rely on placeholder text alone as it disappears on input and is inaccessible. Use errorText (not helperText) to surface validation failures; the component applies danger styling automatically. leftElement and rightElement accept icons or small controls (e.g. currency symbol, clear button).",props:[{name:"type",type:"enum",required:!1,default:"text",description:"HTML input type.",enumValues:["text","number","password","email","tel","url","search"]},{name:"label",type:"string",required:!1,description:"Visible label rendered above the input."},{name:"helperText",type:"string",required:!1,description:"Supplementary hint shown below the input."},{name:"errorText",type:"string",required:!1,description:"Validation error message. When set, input renders in error state."},{name:"leftElement",type:"ReactNode",required:!1,description:"Icon or adornment rendered inside the left edge."},{name:"rightElement",type:"ReactNode",required:!1,description:"Icon or adornment rendered inside the right edge."},{name:"placeholder",type:"string",required:!1,description:"Placeholder text. Use as a hint, not a label."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the input."},{name:"value",type:"string",required:!1,description:"Controlled value."},{name:"onChange",type:"function",required:!1,description:"Change handler."}],usageExamples:[{title:"Basic",code:'<Input label="Email" type="email" placeholder="you@example.com" />'},{title:"With helper text",code:'<Input label="Username" helperText="3–20 characters, letters and numbers only" />'},{title:"Error state",code:'<Input label="Password" type="password" value={value} errorText="Must be at least 8 characters" />'},{title:"With icon",code:'<Input label="Search" leftElement={<SearchIcon />} placeholder="Search…" />'}],compositionGraph:[],accessibility:{role:"textbox",ariaAttributes:["aria-invalid","aria-describedby","aria-label"],keyboardInteractions:["Tab — focuses the input"]}},$=g.forwardRef(({label:n,helperText:t,errorText:a,autoResize:r=!1,maxLength:o,showCount:i=!1,id:s,value:l,onChange:d,style:p,...f},m)=>{const c=g.useRef(null),u=m??c,b=s??`lucent-textarea-${Math.random().toString(36).slice(2,7)}`,h=!!a,x=typeof l=="string"?l.length:0;return g.useEffect(()=>{if(!r)return;const w=u.current;w&&(w.style.height="auto",w.style.height=`${w.scrollHeight}px`)},[l,r,u]),e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",width:"100%"},children:[n&&e.jsx("label",{htmlFor:b,style:{fontSize:"var(--lucent-font-size-sm)",fontWeight:"var(--lucent-font-weight-medium)",color:"var(--lucent-text-primary)",fontFamily:"var(--lucent-font-family-base)"},children:n}),e.jsx("textarea",{ref:u,id:b,maxLength:o,value:l,onChange:d,"aria-invalid":h,"aria-describedby":h?`${b}-error`:t?`${b}-helper`:void 0,style:{width:"100%",minHeight:"100px",padding:"var(--lucent-space-3)",fontSize:"var(--lucent-font-size-md)",fontFamily:"var(--lucent-font-family-base)",color:"var(--lucent-text-primary)",background:"var(--lucent-surface-default)",border:`1px solid ${h?"var(--lucent-danger-default)":"var(--lucent-border-default)"}`,borderRadius:"var(--lucent-radius-md)",outline:"none",resize:r?"none":"vertical",boxSizing:"border-box",lineHeight:"var(--lucent-line-height-base)",transition:"border-color var(--lucent-duration-fast) var(--lucent-easing-default)",...p},onFocus:w=>{var v;w.currentTarget.style.borderColor=h?"var(--lucent-danger-default)":"var(--lucent-focus-ring)",w.currentTarget.style.boxShadow=`0 0 0 3px ${h?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`,(v=f.onFocus)==null||v.call(f,w)},onBlur:w=>{var v;w.currentTarget.style.borderColor=h?"var(--lucent-danger-default)":"var(--lucent-border-default)",w.currentTarget.style.boxShadow="none",(v=f.onBlur)==null||v.call(f,w)},...f}),e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start"},children:[e.jsxs("div",{children:[h&&e.jsx("span",{id:`${b}-error`,role:"alert",style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:a}),!h&&t&&e.jsx("span",{id:`${b}-helper`,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)"},children:t})]}),(i||o)&&e.jsxs("span",{style:{fontSize:"var(--lucent-font-size-xs)",color:o&&x>=o?"var(--lucent-danger-text)":"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-mono)",flexShrink:0,marginLeft:"var(--lucent-space-2)"},children:[x,o?`/${o}`:""]})]})]})});$.displayName="Textarea";const se={id:"textarea",name:"Textarea",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A multi-line text input with optional auto-resize and character count.",designIntent:"Use autoResize for open-ended fields (bio, description) where content length is unpredictable. Use maxLength + showCount for fields with hard limits (tweet-style). Behaves identically to Input for label/helper/error patterns.",props:[{name:"label",type:"string",required:!1,description:"Visible label above the textarea."},{name:"helperText",type:"string",required:!1,description:"Hint text shown below."},{name:"errorText",type:"string",required:!1,description:"Validation error. Triggers error styling."},{name:"autoResize",type:"boolean",required:!1,default:"false",description:"Grows with content, disables manual resize handle."},{name:"maxLength",type:"number",required:!1,description:"Character limit. Displays counter when set."},{name:"showCount",type:"boolean",required:!1,default:"false",description:"Always show character counter even without maxLength."},{name:"value",type:"string",required:!1,description:"Controlled value."},{name:"onChange",type:"function",required:!1,description:"Change handler."},{name:"placeholder",type:"string",required:!1,description:"Placeholder text."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the textarea."}],usageExamples:[{title:"Basic",code:'<Textarea label="Bio" placeholder="Tell us about yourself…" />'},{title:"Auto-resize",code:'<Textarea label="Description" autoResize value={value} onChange={e => setValue(e.target.value)} />'},{title:"With character count",code:'<Textarea label="Tweet" maxLength={280} showCount value={value} onChange={e => setValue(e.target.value)} />'},{title:"Error state",code:'<Textarea label="Notes" errorText="Required" value="" />'}],compositionGraph:[],accessibility:{role:"textbox",ariaAttributes:["aria-multiline","aria-invalid","aria-describedby"],keyboardInteractions:["Tab — focuses the textarea"]}},le={neutral:{bg:"var(--lucent-bg-muted)",color:"var(--lucent-text-secondary)",border:"var(--lucent-border-default)"},accent:{bg:"var(--lucent-accent-default)",color:"var(--lucent-text-on-accent)",border:"var(--lucent-accent-default)"},success:{bg:"var(--lucent-success-subtle)",color:"var(--lucent-success-text)",border:"var(--lucent-success-subtle)"},warning:{bg:"var(--lucent-warning-subtle)",color:"var(--lucent-warning-text)",border:"var(--lucent-warning-subtle)"},danger:{bg:"var(--lucent-danger-subtle)",color:"var(--lucent-danger-text)",border:"var(--lucent-danger-subtle)"},info:{bg:"var(--lucent-info-subtle)",color:"var(--lucent-info-text)",border:"var(--lucent-info-subtle)"}},ce={sm:{fontSize:"var(--lucent-font-size-xs)",padding:"0 var(--lucent-space-2)",height:"18px"},md:{fontSize:"var(--lucent-font-size-sm)",padding:"0 var(--lucent-space-2)",height:"22px"}};function de({variant:n="neutral",size:t="md",dot:a=!1,children:r,style:o}){const i=le[n],s=ce[t];return e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-1)",height:s.height,padding:s.padding,fontSize:s.fontSize,fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-medium)",lineHeight:1,borderRadius:"var(--lucent-radius-full)",background:i.bg,color:i.color,border:`1px solid ${i.border}`,whiteSpace:"nowrap",boxSizing:"border-box",...o},children:[a&&e.jsx("span",{style:{width:6,height:6,borderRadius:"var(--lucent-radius-full)",background:"currentColor",flexShrink:0}}),r]})}const ue={id:"badge",name:"Badge",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A small inline label for status, count, or category.",designIntent:'Badges communicate status or category at a glance. Match variant to semantic meaning — never use "danger" for non-critical states or "success" for neutral counts. Use dot=true when a single colour indicator is enough context (e.g. online status). Keep badge text short: 1–3 words maximum.',props:[{name:"variant",type:"enum",required:!1,default:"neutral",description:"Colour scheme conveying semantic meaning.",enumValues:["neutral","success","warning","danger","info","accent"]},{name:"size",type:"enum",required:!1,default:"md",description:"Controls height and font size.",enumValues:["sm","md"]},{name:"dot",type:"boolean",required:!1,default:"false",description:"Prepends a coloured dot indicator."},{name:"children",type:"ReactNode",required:!0,description:"Badge label."}],usageExamples:[{title:"Status",code:'<Badge variant="success" dot>Active</Badge>'},{title:"Count",code:'<Badge variant="danger">12</Badge>'},{title:"Category",code:'<Badge variant="info">Beta</Badge>'},{title:"Neutral tag",code:"<Badge>Draft</Badge>"}],compositionGraph:[],accessibility:{role:"status",notes:"Use aria-label on the parent element when badge meaning depends on context."}},pe={xs:24,sm:32,md:40,lg:56,xl:80},fe={xs:"var(--lucent-font-size-xs)",sm:"var(--lucent-font-size-xs)",md:"var(--lucent-font-size-sm)",lg:"var(--lucent-font-size-lg)",xl:"var(--lucent-font-size-xl)"};function me(n,t){var r,o,i;if(t)return t.slice(0,2).toUpperCase();const a=n.trim().split(/\s+/);return a.length===1?(((r=a[0])==null?void 0:r[0])??"").toUpperCase():((((o=a[0])==null?void 0:o[0])??"")+(((i=a[a.length-1])==null?void 0:i[0])??"")).toUpperCase()}function he({src:n,alt:t,size:a="md",initials:r,style:o,...i}){const s=pe[a],l=me(t,r),d={width:s,height:s,borderRadius:"var(--lucent-radius-full)",flexShrink:0,display:"inline-flex",alignItems:"center",justifyContent:"center",overflow:"hidden",boxSizing:"border-box",userSelect:"none",...o};return n?e.jsx("img",{src:n,alt:t,width:s,height:s,style:{...d,objectFit:"cover"},...i}):e.jsx("span",{role:"img","aria-label":t,style:{...d,background:"var(--lucent-accent-default)",color:"var(--lucent-text-on-accent)",fontSize:fe[a],fontWeight:"var(--lucent-font-weight-semibold)",fontFamily:"var(--lucent-font-family-base)"},children:l})}const ge={id:"avatar",name:"Avatar",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A circular user image with initials fallback.",designIntent:"Always provide alt for accessibility — it is used to derive initials automatically when src is absent or fails. Use initials prop to override auto-derived initials (e.g. for non-Latin names). Size xs/sm suit table rows and compact lists; md is the default for comment threads; lg/xl for profile headers.",props:[{name:"src",type:"string",required:!1,description:"Image URL. Falls back to initials if omitted or fails to load."},{name:"alt",type:"string",required:!0,description:"Alt text and source for auto-derived initials."},{name:"size",type:"enum",required:!1,default:"md",description:"Diameter of the avatar.",enumValues:["xs","sm","md","lg","xl"]},{name:"initials",type:"string",required:!1,description:"Override auto-derived initials (max 2 characters)."}],usageExamples:[{title:"With image",code:'<Avatar src="/avatars/jane.jpg" alt="Jane Doe" />'},{title:"Initials fallback",code:'<Avatar alt="Jane Doe" />'},{title:"Large profile",code:'<Avatar src={user.avatar} alt={user.name} size="lg" />'},{title:"Custom initials",code:'<Avatar alt="张伟" initials="张" size="md" />'}],compositionGraph:[],accessibility:{role:"img",ariaAttributes:["aria-label"],notes:'When src is present, renders as <img> with alt. When showing initials, renders as <span role="img" aria-label>.'}},be={xs:12,sm:16,md:24,lg:36},ve={xs:2.5,sm:2.5,md:2,lg:2};function L({size:n="md",label:t="Loading…",color:a}){const r=be[n],o=ve[n];return e.jsxs("span",{role:"status","aria-label":t,style:{display:"inline-flex",alignItems:"center",justifyContent:"center"},children:[e.jsxs("svg",{width:r,height:r,viewBox:"0 0 24 24",fill:"none","aria-hidden":!0,style:{animation:"lucent-spin 0.7s linear infinite",color:a??"currentColor"},children:[e.jsx("style",{children:"@keyframes lucent-spin { to { transform: rotate(360deg); } }"}),e.jsx("circle",{cx:12,cy:12,r:10,stroke:"currentColor",strokeWidth:o,strokeOpacity:.2}),e.jsx("path",{d:"M12 2a10 10 0 0 1 10 10",stroke:"currentColor",strokeWidth:o,strokeLinecap:"round"})]}),e.jsx("span",{style:{position:"absolute",width:1,height:1,overflow:"hidden",clip:"rect(0,0,0,0)",whiteSpace:"nowrap"},children:t})]})}const ye={id:"spinner",name:"Spinner",tier:"atom",domain:"neutral",specVersion:"0.1",description:"An animated loading indicator for async operations.",designIntent:"Use Spinner for indeterminate loading states of short duration (< 3s). For full-page or skeleton-level loading, prefer Skeleton instead. The label prop is visually hidden but read by screen readers — always set it to a meaningful description of what is loading.",props:[{name:"size",type:"enum",required:!1,default:"md",description:"Spinner diameter.",enumValues:["xs","sm","md","lg"]},{name:"label",type:"string",required:!1,default:"Loading…",description:"Visually hidden accessible label."},{name:"color",type:"string",required:!1,description:"Override colour (CSS value). Defaults to currentColor."}],usageExamples:[{title:"Default",code:"<Spinner />"},{title:"Inside button",code:'<Button loading><Spinner size="sm" label="Saving…" /></Button>'},{title:"Full-page overlay",code:`<div style={{ display: 'grid', placeItems: 'center', minHeight: '100vh' }}><Spinner size="lg" label="Loading dashboard…" /></div>`}],compositionGraph:[],accessibility:{role:"status",ariaAttributes:["aria-label"],notes:'The visible SVG is aria-hidden. The label is conveyed via a visually-hidden span inside role="status".'}};function xe({orientation:n="horizontal",label:t,spacing:a="var(--lucent-space-4)",style:r}){return n==="vertical"?e.jsx("span",{role:"separator","aria-orientation":"vertical",style:{display:"inline-block",width:"1px",alignSelf:"stretch",background:"var(--lucent-border-default)",margin:`0 ${a}`,flexShrink:0,...r}}):t?e.jsxs("div",{role:"separator","aria-label":t,style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-3)",margin:`${a} 0`,...r},children:[e.jsx("span",{style:{flex:1,height:"1px",background:"var(--lucent-border-default)"}}),e.jsx("span",{style:{fontSize:"var(--lucent-font-size-xs)",fontFamily:"var(--lucent-font-family-base)",color:"var(--lucent-text-secondary)",whiteSpace:"nowrap",letterSpacing:"var(--lucent-letter-spacing-wide)",textTransform:"uppercase"},children:t}),e.jsx("span",{style:{flex:1,height:"1px",background:"var(--lucent-border-default)"}})]}):e.jsx("hr",{role:"separator",style:{border:"none",borderTop:"1px solid var(--lucent-border-default)",margin:`${a} 0`,width:"100%",...r}})}const we={id:"divider",name:"Divider",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A visual separator between content sections, horizontal or vertical.",designIntent:'Use horizontal Divider to separate sections in a layout. Use vertical Divider inline between sibling elements (e.g. nav links, toolbar buttons). Use the label prop for "OR" separators in auth flows or form sections — never use a plain text node next to a divider for this.',props:[{name:"orientation",type:"enum",required:!1,default:"horizontal",description:"Direction of the divider line.",enumValues:["horizontal","vertical"]},{name:"label",type:"string",required:!1,description:'Optional centered label (horizontal only). Common use: "OR", "AND", section titles.'},{name:"spacing",type:"string",required:!1,default:"var(--lucent-space-4)",description:"Margin on the axis perpendicular to the line."}],usageExamples:[{title:"Section separator",code:"<Divider />"},{title:"With label",code:'<Divider label="OR" />'},{title:"Vertical in nav",code:`<nav style={{ display: 'flex', alignItems: 'center' }}><a>Home</a><Divider orientation="vertical" /><a>About</a></nav>`}],compositionGraph:[],accessibility:{role:"separator",ariaAttributes:["aria-orientation","aria-label"]}},Se={sm:14,md:16},ke=`
2
2
  @keyframes lucent-cb-pop {
3
3
  0% { transform: scale(1); }
4
4
  35% { transform: scale(0.82); }
@@ -114,4 +114,4 @@ ${a}
114
114
  }`}function Bt(n){const t=parseInt(n.slice(1,3),16)/255,a=parseInt(n.slice(3,5),16)/255,r=parseInt(n.slice(5,7),16)/255,o=i=>i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4);return .2126*o(t)+.7152*o(a)+.0722*o(r)}function Y(n){return Bt(n)<.179?"#ffffff":"#000000"}const X=g.createContext({theme:"light",tokens:R});function $t({theme:n="light",tokens:t,children:a}){const r=g.useId().replace(/:/g,""),o=n==="dark"?_:R,i=t?{...o,...t}:o,s={...i,textOnAccent:(t==null?void 0:t.textOnAccent)??Y(i.accentDefault)},l=`html { font-size: 13px; }
115
115
  `+K(s,":root");return g.useLayoutEffect(()=>{let d=document.getElementById(`lucent-tokens-${r}`);return d||(d=document.createElement("style"),d.id=`lucent-tokens-${r}`,document.head.appendChild(d)),d.textContent=l,()=>{var p;(p=document.getElementById(`lucent-tokens-${r}`))==null||p.remove()}},[r,l]),e.jsx(X.Provider,{value:{theme:n,tokens:s},children:a})}function Lt(){return g.useContext(X)}const Dt={accentDefault:"#e9c96b",accentHover:"#ddb84e",accentActive:"#c9a33b",accentSubtle:"#fef9ec",focusRing:"#e9c96b"};function C(n,t){return{field:n,message:t}}function J(n){const t=[];if(typeof n!="object"||n===null)return{valid:!1,errors:[C("manifest","Must be a non-null object")]};const a=n,r=["id","name","description","designIntent","specVersion"];for(const i of r)(typeof a[i]!="string"||a[i].trim()==="")&&t.push(C(i,"Must be a non-empty string"));typeof a.id=="string"&&!/^[a-z][a-z0-9-]*$/.test(a.id)&&t.push(C("id",'Must be kebab-case (e.g. "button", "form-field")'));const o=["atom","molecule","block","flow","overlay"];return o.includes(a.tier)||t.push(C("tier",`Must be one of: ${o.join(", ")}`)),(typeof a.domain!="string"||a.domain.trim()==="")&&t.push(C("domain","Must be a non-empty string")),Array.isArray(a.props)?a.props.forEach((i,s)=>{const l=i,d=`props[${s}]`;(typeof l.name!="string"||l.name==="")&&t.push(C(`${d}.name`,"Must be a non-empty string")),(typeof l.type!="string"||l.type==="")&&t.push(C(`${d}.type`,"Must be a non-empty string")),typeof l.required!="boolean"&&t.push(C(`${d}.required`,"Must be a boolean")),(typeof l.description!="string"||l.description==="")&&t.push(C(`${d}.description`,"Must be a non-empty string"))}):t.push(C("props","Must be an array")),Array.isArray(a.usageExamples)?a.usageExamples.length===0?t.push(C("usageExamples","Must have at least one example")):a.usageExamples.forEach((i,s)=>{const l=i,d=`usageExamples[${s}]`;(typeof l.title!="string"||l.title==="")&&t.push(C(`${d}.title`,"Must be a non-empty string")),(typeof l.code!="string"||l.code==="")&&t.push(C(`${d}.code`,"Must be a non-empty string"))}):t.push(C("usageExamples","Must be an array")),Array.isArray(a.compositionGraph)||t.push(C("compositionGraph","Must be an array (empty array is fine for atoms)")),typeof a.specVersion=="string"&&!/^\d+\.\d+$/.test(a.specVersion)&&t.push(C("specVersion",'Must be "MAJOR.MINOR" format, e.g. "0.1"')),{valid:t.length===0,errors:t}}function Vt(n){const t=J(n);if(!t.valid){const a=t.errors.map(r=>` ${r.field}: ${r.message}`).join(`
116
116
  `);throw new Error(`Invalid ComponentManifest:
117
- ${a}`)}}function Wt(n){if(typeof n!="object"||n===null)return!1;const t=n;return typeof t.name=="string"&&typeof t.type=="string"&&typeof t.required=="boolean"&&typeof t.description=="string"}const Pt="0.1",Ot="0.1.0";exports.Alert=yt;exports.AlertManifest=xt;exports.Avatar=he;exports.AvatarManifest=ge;exports.Badge=de;exports.BadgeManifest=ue;exports.Breadcrumb=jt;exports.Button=B;exports.ButtonManifest=oe;exports.Card=dt;exports.CardManifest=ut;exports.Checkbox=D;exports.CheckboxManifest=Te;exports.Collapsible=qt;exports.Divider=xe;exports.DividerManifest=we;exports.EmptyState=wt;exports.EmptyStateManifest=St;exports.FormField=tt;exports.FormFieldManifest=nt;exports.Icon=Ue;exports.IconManifest=_e;exports.Input=E;exports.InputManifest=ie;exports.LUCENT_UI_VERSION=Ot;exports.LucentProvider=$t;exports.MANIFEST_SPEC_VERSION=Pt;exports.NavLink=et;exports.PageLayout=Rt;exports.Radio=je;exports.RadioGroup=W;exports.RadioGroupUncontrolled=ze;exports.RadioManifest=Me;exports.SearchInput=ot;exports.SearchInputManifest=it;exports.Select=P;exports.SelectManifest=Be;exports.Skeleton=Ct;exports.SkeletonManifest=It;exports.Spinner=L;exports.SpinnerManifest=ye;exports.Tabs=zt;exports.Tag=De;exports.TagManifest=Ve;exports.Text=z;exports.TextManifest=Ze;exports.Textarea=$;exports.TextareaManifest=se;exports.Toggle=Re;exports.ToggleManifest=Fe;exports.Tooltip=Oe;exports.TooltipManifest=He;exports.assertManifest=Vt;exports.brandTokens=Dt;exports.darkTokens=_;exports.getContrastText=Y;exports.isValidPropDescriptor=Wt;exports.lightTokens=R;exports.makeLibraryCSS=K;exports.useLucent=Lt;exports.validateManifest=J;
117
+ ${a}`)}}function Wt(n){if(typeof n!="object"||n===null)return!1;const t=n;return typeof t.name=="string"&&typeof t.type=="string"&&typeof t.required=="boolean"&&typeof t.description=="string"}const Pt="1.0",Ot="0.1.0";exports.Alert=yt;exports.AlertManifest=xt;exports.Avatar=he;exports.AvatarManifest=ge;exports.Badge=de;exports.BadgeManifest=ue;exports.Breadcrumb=jt;exports.Button=B;exports.ButtonManifest=oe;exports.Card=dt;exports.CardManifest=ut;exports.Checkbox=D;exports.CheckboxManifest=Te;exports.Collapsible=qt;exports.Divider=xe;exports.DividerManifest=we;exports.EmptyState=wt;exports.EmptyStateManifest=St;exports.FormField=tt;exports.FormFieldManifest=nt;exports.Icon=Ue;exports.IconManifest=_e;exports.Input=E;exports.InputManifest=ie;exports.LUCENT_UI_VERSION=Ot;exports.LucentProvider=$t;exports.MANIFEST_SPEC_VERSION=Pt;exports.NavLink=et;exports.PageLayout=Rt;exports.Radio=je;exports.RadioGroup=W;exports.RadioGroupUncontrolled=ze;exports.RadioManifest=Me;exports.SearchInput=ot;exports.SearchInputManifest=it;exports.Select=P;exports.SelectManifest=Be;exports.Skeleton=Ct;exports.SkeletonManifest=It;exports.Spinner=L;exports.SpinnerManifest=ye;exports.Tabs=zt;exports.Tag=De;exports.TagManifest=Ve;exports.Text=z;exports.TextManifest=Ze;exports.Textarea=$;exports.TextareaManifest=se;exports.Toggle=Re;exports.ToggleManifest=Fe;exports.Tooltip=Oe;exports.TooltipManifest=He;exports.assertManifest=Vt;exports.brandTokens=Dt;exports.darkTokens=_;exports.getContrastText=Y;exports.isValidPropDescriptor=Wt;exports.lightTokens=R;exports.makeLibraryCSS=K;exports.useLucent=Lt;exports.validateManifest=J;
package/dist/index.d.ts CHANGED
@@ -362,7 +362,7 @@ export declare interface LucentTokens extends SemanticColorTokens, TypographyTok
362
362
  */
363
363
  export declare function makeLibraryCSS(tokens: LucentTokens, selector?: string): string;
364
364
 
365
- export declare const MANIFEST_SPEC_VERSION = "0.1";
365
+ export declare const MANIFEST_SPEC_VERSION = "1.0";
366
366
 
367
367
  declare interface MotionTokens {
368
368
  durationFast: string;
package/dist/index.js CHANGED
@@ -118,7 +118,7 @@ const st = {
118
118
  name: "Button",
119
119
  tier: "atom",
120
120
  domain: "neutral",
121
- specVersion: "0.1",
121
+ specVersion: "1.0",
122
122
  description: "A clickable control that triggers an action. The primary interactive primitive in Lucent UI.",
123
123
  designIntent: 'Buttons communicate available actions. Variant conveys hierarchy: use "primary" for the single most important action in a view, "secondary" for supporting actions, "ghost" for low-emphasis actions in dense UIs, and "danger" exclusively for destructive or irreversible operations. Size should match surrounding content density — prefer "md" as the default and reserve "sm" for toolbars or tables.',
124
124
  props: [
@@ -3567,7 +3567,7 @@ function Zt(t) {
3567
3567
  const e = t;
3568
3568
  return typeof e.name == "string" && typeof e.type == "string" && typeof e.required == "boolean" && typeof e.description == "string";
3569
3569
  }
3570
- const en = "0.1", tn = "0.1.0";
3570
+ const en = "1.0", tn = "0.1.0";
3571
3571
  export {
3572
3572
  Vt as Alert,
3573
3573
  Wt as AlertManifest,
@@ -0,0 +1,41 @@
1
+ // ─── Figma Variables API types ────────────────────────────────────────────────
2
+ // ─── API client ───────────────────────────────────────────────────────────────
3
+ export async function fetchFigmaVariables(figmaToken, fileKey) {
4
+ const url = `https://api.figma.com/v1/files/${fileKey}/variables/local`;
5
+ const res = await fetch(url, {
6
+ headers: {
7
+ 'X-Figma-Token': figmaToken,
8
+ },
9
+ });
10
+ if (!res.ok) {
11
+ const body = await res.text().catch(() => '');
12
+ throw new Error(`Figma API error ${res.status}: ${res.statusText}${body ? `\n${body}` : ''}`);
13
+ }
14
+ return res.json();
15
+ }
16
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
17
+ /** Convert Figma RGBA (0–1 channels) to a CSS hex string. */
18
+ export function figmaColorToHex(color) {
19
+ const toHex = (n) => Math.round(Math.min(1, Math.max(0, n)) * 255)
20
+ .toString(16)
21
+ .padStart(2, '0');
22
+ const rgb = `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
23
+ // Include alpha channel only when not fully opaque
24
+ if (color.a < 0.9999) {
25
+ return `${rgb}${toHex(color.a)}`;
26
+ }
27
+ return rgb;
28
+ }
29
+ /** True if value is a FigmaColor object (has r/g/b/a number fields). */
30
+ export function isFigmaColor(v) {
31
+ return (typeof v === 'object' &&
32
+ v !== null &&
33
+ 'r' in v &&
34
+ typeof v.r === 'number');
35
+ }
36
+ /** True if value is a VARIABLE_ALIAS reference. */
37
+ export function isAlias(v) {
38
+ return (typeof v === 'object' &&
39
+ v !== null &&
40
+ v.type === 'VARIABLE_ALIAS');
41
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lucent-manifest init
4
+ *
5
+ * Usage:
6
+ * npx lucent-manifest init --figma-token <token> --file-key <key> [options]
7
+ * npx lucent-manifest init --template (manual fallback)
8
+ *
9
+ * Options:
10
+ * --figma-token <token> Figma personal access token
11
+ * --file-key <key> Figma file key (from the file URL)
12
+ * --light-mode <name> Mode name to treat as light theme (default: "light")
13
+ * --dark-mode <name> Mode name to treat as dark theme (default: "dark")
14
+ * --name <name> Design system name written into the manifest
15
+ * --out <path> Output file path (default: lucent.manifest.json)
16
+ * --template Write an empty JSON template instead of fetching Figma
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fetchFigmaVariables } from './figma.js';
21
+ import { mapFigmaToTokens } from './mapper.js';
22
+ // ─── Arg parsing ──────────────────────────────────────────────────────────────
23
+ function parseArgs(argv) {
24
+ const args = {};
25
+ for (let i = 0; i < argv.length; i++) {
26
+ const arg = argv[i];
27
+ if (arg.startsWith('--')) {
28
+ const key = arg.slice(2);
29
+ const next = argv[i + 1];
30
+ if (next !== undefined && !next.startsWith('--')) {
31
+ args[key] = next;
32
+ i++;
33
+ }
34
+ else {
35
+ args[key] = true;
36
+ }
37
+ }
38
+ }
39
+ return args;
40
+ }
41
+ function required(args, key) {
42
+ const v = args[key];
43
+ if (!v || v === true) {
44
+ console.error(`Error: --${key} is required.`);
45
+ process.exit(1);
46
+ }
47
+ return v;
48
+ }
49
+ function optional(args, key, fallback) {
50
+ const v = args[key];
51
+ return typeof v === 'string' ? v : fallback;
52
+ }
53
+ // ─── Template fallback ────────────────────────────────────────────────────────
54
+ function writeTemplate(outPath) {
55
+ const templateSrc = new URL('./template.manifest.json', import.meta.url);
56
+ fs.copyFileSync(templateSrc, outPath);
57
+ console.log(`Template written to ${outPath}`);
58
+ console.log('Fill in the token values and load it with LucentProvider.');
59
+ }
60
+ // ─── Main ─────────────────────────────────────────────────────────────────────
61
+ async function main() {
62
+ const args = parseArgs(process.argv.slice(2));
63
+ const outPath = path.resolve(optional(args, 'out', 'lucent.manifest.json'));
64
+ if (args['template']) {
65
+ writeTemplate(outPath);
66
+ return;
67
+ }
68
+ const figmaToken = required(args, 'figma-token');
69
+ const fileKey = required(args, 'file-key');
70
+ const lightModeName = optional(args, 'light-mode', 'light');
71
+ const darkModeName = optional(args, 'dark-mode', 'dark');
72
+ const systemName = optional(args, 'name', 'My Design System');
73
+ console.log(`Fetching Figma variables for file ${fileKey}…`);
74
+ const response = await fetchFigmaVariables(figmaToken, fileKey);
75
+ const manifest = {
76
+ version: '1.0',
77
+ name: systemName,
78
+ tokens: {},
79
+ };
80
+ // Light theme
81
+ try {
82
+ const { tokens: lightTokens, unmapped: lightUnmapped } = mapFigmaToTokens(response, lightModeName);
83
+ manifest.tokens.light = lightTokens;
84
+ console.log(`Light theme: mapped ${Object.keys(lightTokens).length} token(s).`);
85
+ if (lightUnmapped.length > 0) {
86
+ console.warn(` ${lightUnmapped.length} variable(s) didn't match a Lucent token key and were skipped:`);
87
+ for (const u of lightUnmapped) {
88
+ console.warn(` "${u.figmaName}" → candidate "${u.candidateKey}" (value: ${u.value})`);
89
+ }
90
+ }
91
+ }
92
+ catch (err) {
93
+ console.warn(`Skipping light theme: ${err.message}`);
94
+ }
95
+ // Dark theme
96
+ try {
97
+ const { tokens: darkTokens, unmapped: darkUnmapped } = mapFigmaToTokens(response, darkModeName);
98
+ manifest.tokens.dark = darkTokens;
99
+ console.log(`Dark theme: mapped ${Object.keys(darkTokens).length} token(s).`);
100
+ if (darkUnmapped.length > 0) {
101
+ console.warn(` ${darkUnmapped.length} variable(s) didn't match a Lucent token key and were skipped:`);
102
+ for (const u of darkUnmapped) {
103
+ console.warn(` "${u.figmaName}" → candidate "${u.candidateKey}" (value: ${u.value})`);
104
+ }
105
+ }
106
+ }
107
+ catch (err) {
108
+ console.warn(`Skipping dark theme: ${err.message}`);
109
+ }
110
+ if (!manifest.tokens.light && !manifest.tokens.dark) {
111
+ console.error('No tokens were mapped. Check your --light-mode and --dark-mode names match the Figma file.');
112
+ process.exit(1);
113
+ }
114
+ fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
115
+ console.log(`\nManifest written to ${outPath}`);
116
+ }
117
+ main().catch(err => {
118
+ console.error(err instanceof Error ? err.message : String(err));
119
+ process.exit(1);
120
+ });
@@ -0,0 +1,162 @@
1
+ import { figmaColorToHex, isFigmaColor, isAlias, } from './figma.js';
2
+ // ─── Token key registry ───────────────────────────────────────────────────────
3
+ /**
4
+ * Every valid key in LucentTokens — used to validate candidate names produced
5
+ * by the name normaliser before writing them into the output manifest.
6
+ */
7
+ const LUCENT_TOKEN_KEYS = new Set([
8
+ // SemanticColorTokens
9
+ 'bgBase', 'bgSubtle', 'bgMuted', 'bgOverlay',
10
+ 'surfaceDefault', 'surfaceRaised', 'surfaceOverlay',
11
+ 'borderDefault', 'borderSubtle', 'borderStrong',
12
+ 'textPrimary', 'textSecondary', 'textDisabled', 'textInverse', 'textOnAccent',
13
+ 'accentDefault', 'accentHover', 'accentActive', 'accentSubtle',
14
+ 'successDefault', 'successSubtle', 'successText',
15
+ 'warningDefault', 'warningSubtle', 'warningText',
16
+ 'dangerDefault', 'dangerHover', 'dangerSubtle', 'dangerText',
17
+ 'infoDefault', 'infoSubtle', 'infoText',
18
+ 'focusRing',
19
+ // TypographyTokens
20
+ 'fontFamilyBase', 'fontFamilyMono', 'fontFamilyDisplay',
21
+ 'fontSizeXs', 'fontSizeSm', 'fontSizeMd', 'fontSizeLg',
22
+ 'fontSizeXl', 'fontSize2xl', 'fontSize3xl',
23
+ 'fontWeightRegular', 'fontWeightMedium', 'fontWeightSemibold', 'fontWeightBold',
24
+ 'lineHeightTight', 'lineHeightBase', 'lineHeightRelaxed',
25
+ 'letterSpacingTight', 'letterSpacingBase', 'letterSpacingWide',
26
+ // SpacingTokens
27
+ 'space0', 'space1', 'space2', 'space3', 'space4', 'space5',
28
+ 'space6', 'space8', 'space10', 'space12', 'space16', 'space20', 'space24',
29
+ // RadiusTokens
30
+ 'radiusNone', 'radiusSm', 'radiusMd', 'radiusLg', 'radiusXl', 'radiusFull',
31
+ // ShadowTokens
32
+ 'shadowNone', 'shadowSm', 'shadowMd', 'shadowLg', 'shadowXl',
33
+ // MotionTokens
34
+ 'durationFast', 'durationBase', 'durationSlow',
35
+ 'easingDefault', 'easingEmphasized', 'easingDecelerate',
36
+ ]);
37
+ // ─── Name normalisation ───────────────────────────────────────────────────────
38
+ /**
39
+ * Top-level Figma collection category prefixes that carry no semantic meaning
40
+ * in the Lucent token schema and should be dropped before camelCasing.
41
+ */
42
+ const CATEGORY_PREFIXES = new Set([
43
+ 'color', 'colour', 'typography', 'type', 'spacing', 'space',
44
+ 'radius', 'shadow', 'motion', 'animation',
45
+ ]);
46
+ /**
47
+ * Convert a Figma variable name (slash-separated path, may include hyphens)
48
+ * into a camelCase candidate token key.
49
+ *
50
+ * Examples:
51
+ * "color/bg/base" → "bgBase"
52
+ * "color/text/primary" → "textPrimary"
53
+ * "typography/font-size/xl" → "fontSizeXl"
54
+ * "spacing/space-4" → "space4"
55
+ * "radius/radius-md" → "radiusMd"
56
+ * "accentDefault" → "accentDefault" (no change needed)
57
+ */
58
+ export function normalizeName(figmaName) {
59
+ // Split path segments on /
60
+ const segments = figmaName.split('/').map(s => s.trim()).filter(Boolean);
61
+ // Drop a leading category prefix if present
62
+ if (segments.length > 1 && CATEGORY_PREFIXES.has(segments[0].toLowerCase())) {
63
+ segments.shift();
64
+ }
65
+ // Further split each segment on hyphens
66
+ const parts = segments.flatMap(s => s.split('-').filter(Boolean));
67
+ if (parts.length === 0)
68
+ return figmaName;
69
+ // camelCase: lowercase first part, title-case the rest
70
+ return parts
71
+ .map((p, i) => i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
72
+ .join('');
73
+ }
74
+ // ─── Alias resolution ─────────────────────────────────────────────────────────
75
+ /**
76
+ * Resolve a variable value for a given mode, following VARIABLE_ALIAS chains.
77
+ * Returns `undefined` if the chain cannot be resolved (dangling alias, missing
78
+ * mode, non-string/non-color resolved type).
79
+ */
80
+ function resolveValue(variable, modeId, allVariables, visited = new Set()) {
81
+ if (visited.has(variable.id))
82
+ return undefined; // circular alias guard
83
+ visited.add(variable.id);
84
+ const value = variable.valuesByMode[modeId];
85
+ if (value === undefined)
86
+ return undefined;
87
+ if (isAlias(value)) {
88
+ const target = allVariables[value.id];
89
+ if (!target)
90
+ return undefined;
91
+ // Aliases preserve their own modeId in the target collection — use the
92
+ // same modeId; if absent the target may use its defaultModeId but we
93
+ // can't determine that here without the collections map, so fall back to
94
+ // returning undefined rather than guessing.
95
+ return resolveValue(target, modeId, allVariables, visited);
96
+ }
97
+ return value;
98
+ }
99
+ // ─── Value → CSS string ───────────────────────────────────────────────────────
100
+ function toCssValue(value, resolvedType) {
101
+ if (resolvedType === 'COLOR' && isFigmaColor(value)) {
102
+ return figmaColorToHex(value);
103
+ }
104
+ if (resolvedType === 'FLOAT' && typeof value === 'number') {
105
+ // Figma stores spacing/radius in px without units; emit as "Npx"
106
+ return `${value}px`;
107
+ }
108
+ if (resolvedType === 'STRING' && typeof value === 'string') {
109
+ return value;
110
+ }
111
+ return undefined;
112
+ }
113
+ /**
114
+ * Map a Figma Variables API response for a single named mode (e.g. "light" or
115
+ * "dark") into a `Partial<LucentTokens>` token override object.
116
+ *
117
+ * @param response Full response from `fetchFigmaVariables`
118
+ * @param modeName Case-insensitive mode name to extract (e.g. "Light", "dark")
119
+ */
120
+ export function mapFigmaToTokens(response, modeName) {
121
+ const { variables, variableCollections } = response.meta;
122
+ // Find the modeId matching the requested name across all collections
123
+ const targetModeName = modeName.toLowerCase();
124
+ const modeIdByCollection = new Map();
125
+ for (const collection of Object.values(variableCollections)) {
126
+ const match = collection.modes.find(m => m.name.toLowerCase() === targetModeName);
127
+ if (match) {
128
+ modeIdByCollection.set(collection.id, match.modeId);
129
+ }
130
+ }
131
+ if (modeIdByCollection.size === 0) {
132
+ const available = Object.values(variableCollections)
133
+ .flatMap(c => c.modes.map(m => m.name))
134
+ .filter((v, i, a) => a.indexOf(v) === i)
135
+ .join(', ');
136
+ throw new Error(`No mode named "${modeName}" found in Figma file.\nAvailable modes: ${available || '(none)'}`);
137
+ }
138
+ const tokens = {};
139
+ const unmapped = [];
140
+ for (const variable of Object.values(variables)) {
141
+ // Skip booleans — no LucentTokens field uses boolean values
142
+ if (variable.resolvedType === 'BOOLEAN')
143
+ continue;
144
+ const modeId = modeIdByCollection.get(variable.variableCollectionId);
145
+ if (modeId === undefined)
146
+ continue;
147
+ const raw = resolveValue(variable, modeId, variables);
148
+ if (raw === undefined)
149
+ continue;
150
+ const cssValue = toCssValue(raw, variable.resolvedType);
151
+ if (cssValue === undefined)
152
+ continue;
153
+ const candidate = normalizeName(variable.name);
154
+ if (LUCENT_TOKEN_KEYS.has(candidate)) {
155
+ tokens[candidate] = cssValue;
156
+ }
157
+ else {
158
+ unmapped.push({ figmaName: variable.name, candidateKey: candidate, value: cssValue });
159
+ }
160
+ }
161
+ return { tokens, unmapped };
162
+ }
@@ -0,0 +1,83 @@
1
+ {
2
+ "version": "1.0",
3
+ "name": "My Design System",
4
+ "description": "Optional description of your design system",
5
+ "tokens": {
6
+ "light": {
7
+ "bgBase": "#ffffff",
8
+ "bgSubtle": "#f9fafb",
9
+ "bgMuted": "#f3f4f6",
10
+ "bgOverlay": "rgb(0 0 0 / 0.4)",
11
+ "surfaceDefault": "#ffffff",
12
+ "surfaceRaised": "#ffffff",
13
+ "surfaceOverlay": "#ffffff",
14
+ "borderDefault": "#e5e7eb",
15
+ "borderSubtle": "#f3f4f6",
16
+ "borderStrong": "#9ca3af",
17
+ "textPrimary": "#111827",
18
+ "textSecondary": "#6b7280",
19
+ "textDisabled": "#9ca3af",
20
+ "textInverse": "#ffffff",
21
+ "textOnAccent": "#ffffff",
22
+ "accentDefault": "#111827",
23
+ "accentHover": "#1f2937",
24
+ "accentActive": "#374151",
25
+ "accentSubtle": "#f3f4f6",
26
+ "successDefault": "#16a34a",
27
+ "successSubtle": "#f0fdf4",
28
+ "successText": "#15803d",
29
+ "warningDefault": "#d97706",
30
+ "warningSubtle": "#fffbeb",
31
+ "warningText": "#b45309",
32
+ "dangerDefault": "#dc2626",
33
+ "dangerHover": "#b91c1c",
34
+ "dangerSubtle": "#fef2f2",
35
+ "dangerText": "#b91c1c",
36
+ "infoDefault": "#2563eb",
37
+ "infoSubtle": "#eff6ff",
38
+ "infoText": "#1d4ed8",
39
+ "focusRing": "#111827",
40
+ "fontFamilyBase": "\"Inter\", sans-serif",
41
+ "fontFamilyMono": "\"Fira Code\", monospace",
42
+ "fontFamilyDisplay": "\"Inter\", sans-serif"
43
+ },
44
+ "dark": {
45
+ "bgBase": "#0f172a",
46
+ "bgSubtle": "#1e293b",
47
+ "bgMuted": "#334155",
48
+ "bgOverlay": "rgb(0 0 0 / 0.6)",
49
+ "surfaceDefault": "#1e293b",
50
+ "surfaceRaised": "#334155",
51
+ "surfaceOverlay": "#1e293b",
52
+ "borderDefault": "#334155",
53
+ "borderSubtle": "#1e293b",
54
+ "borderStrong": "#64748b",
55
+ "textPrimary": "#f8fafc",
56
+ "textSecondary": "#94a3b8",
57
+ "textDisabled": "#475569",
58
+ "textInverse": "#0f172a",
59
+ "textOnAccent": "#0f172a",
60
+ "accentDefault": "#f8fafc",
61
+ "accentHover": "#e2e8f0",
62
+ "accentActive": "#cbd5e1",
63
+ "accentSubtle": "#1e293b",
64
+ "successDefault": "#22c55e",
65
+ "successSubtle": "#052e16",
66
+ "successText": "#4ade80",
67
+ "warningDefault": "#f59e0b",
68
+ "warningSubtle": "#431407",
69
+ "warningText": "#fbbf24",
70
+ "dangerDefault": "#ef4444",
71
+ "dangerHover": "#f87171",
72
+ "dangerSubtle": "#450a0a",
73
+ "dangerText": "#f87171",
74
+ "infoDefault": "#3b82f6",
75
+ "infoSubtle": "#0f1f47",
76
+ "infoText": "#60a5fa",
77
+ "focusRing": "#f8fafc",
78
+ "fontFamilyBase": "\"Inter\", sans-serif",
79
+ "fontFamilyMono": "\"Fira Code\", monospace",
80
+ "fontFamilyDisplay": "\"Inter\", sans-serif"
81
+ }
82
+ }
83
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -3,7 +3,7 @@ export const ButtonManifest = {
3
3
  name: 'Button',
4
4
  tier: 'atom',
5
5
  domain: 'neutral',
6
- specVersion: '0.1',
6
+ specVersion: '1.0',
7
7
  description: 'A clickable control that triggers an action. The primary interactive primitive in Lucent UI.',
8
8
  designIntent: 'Buttons communicate available actions. Variant conveys hierarchy: use "primary" for the ' +
9
9
  'single most important action in a view, "secondary" for supporting actions, "ghost" for ' +
@@ -1,3 +1,3 @@
1
1
  export { validateManifest, assertManifest, isValidPropDescriptor, } from './validate.js';
2
2
  export { ButtonManifest } from './examples/button.manifest.js';
3
- export const MANIFEST_SPEC_VERSION = '0.1';
3
+ export const MANIFEST_SPEC_VERSION = '1.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucent-ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "An AI-first React component library with machine-readable manifests.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -15,12 +15,14 @@
15
15
  "./styles": "./dist/index.css"
16
16
  },
17
17
  "bin": {
18
- "lucent-mcp": "./dist-server/server/index.js"
18
+ "lucent-mcp": "./dist-server/server/index.js",
19
+ "lucent-manifest": "./dist-cli/cli/index.js"
19
20
  },
20
21
  "sideEffects": false,
21
22
  "files": [
22
23
  "dist",
23
- "dist-server"
24
+ "dist-server",
25
+ "dist-cli"
24
26
  ],
25
27
  "keywords": [
26
28
  "react",
@@ -55,6 +57,7 @@
55
57
  "dev": "vite --config vite.dev.config.ts",
56
58
  "build": "vite build",
57
59
  "build:server": "tsc -p server/tsconfig.json",
60
+ "build:cli": "tsc -p cli/tsconfig.json && cp cli/template.manifest.json dist-cli/cli/template.manifest.json",
58
61
  "test": "echo \"No tests yet\" && exit 0",
59
62
  "changeset": "changeset",
60
63
  "version-packages": "changeset version",