lucent-ui 0.1.1 → 0.2.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 +22 -17
- package/dist/index.d.ts +75 -0
- package/dist/index.js +916 -514
- package/package.json +1 -1
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"),b=require("react"),J={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)"}},Q={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)"}},F=b.forwardRef(({variant:n="primary",size:t="md",loading:a=!1,fullWidth:r=!1,spread:o=!1,leftIcon:i,rightIcon:l,chevron:s=!1,disableHoverStyles:u=!1,children:f,disabled:p,style:m,...c},d)=>{const h=p??a;return e.jsxs("button",{ref:d,disabled:h,"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:h?"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,...Q[t],...J[n],...m,...h&&{background:"var(--lucent-bg-muted)",color:"var(--lucent-text-disabled)",borderColor:"transparent"}},onMouseEnter:g=>{var y;!h&&!u&&Z(g.currentTarget,n),(y=c.onMouseEnter)==null||y.call(c,g)},onMouseLeave:g=>{var y;!h&&!u&&ee(g.currentTarget,n),(y=c.onMouseLeave)==null||y.call(c,g)},onMouseDown:g=>{var y;h||(g.currentTarget.style.transform="scale(0.95)"),(y=c.onMouseDown)==null||y.call(c,g)},onMouseUp:g=>{var y;g.currentTarget.style.transform="",(y=c.onMouseUp)==null||y.call(c,g)},onFocus:g=>{var y;g.currentTarget.style.boxShadow="0 0 0 3px var(--lucent-accent-subtle)",(y=c.onFocus)==null||y.call(c,g)},onBlur:g=>{var y;g.currentTarget.style.boxShadow="",(y=c.onBlur)==null||y.call(c,g)},...c,children:[i,a?e.jsx(ae,{}):f,!a&&l,!a&&s&&e.jsx(ne,{size:t})]})});F.displayName="Button";function Z(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 ee(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 te={sm:12,md:14,lg:16};function ne({size:n}){const t=te[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 ae(){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 re={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"]}},M=b.forwardRef(({label:n,helperText:t,errorText:a,leftElement:r,rightElement:o,id:i,style:l,...s},u)=>{const f=i??`lucent-input-${Math.random().toString(36).slice(2,7)}`,p=!!a,m=!!s.disabled;return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",width:"100%"},children:[n&&e.jsx("label",{htmlFor:f,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:u,id:f,"aria-invalid":p,"aria-describedby":p?`${f}-error`:t?`${f}-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":p?"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)",...l},onMouseEnter:c=>{var d;!s.disabled&&c.currentTarget!==document.activeElement&&(c.currentTarget.style.borderColor=p?"var(--lucent-danger-default)":"var(--lucent-border-strong)"),(d=s.onMouseEnter)==null||d.call(s,c)},onMouseLeave:c=>{var d;!s.disabled&&c.currentTarget!==document.activeElement&&(c.currentTarget.style.borderColor=p?"var(--lucent-danger-default)":"var(--lucent-border-default)"),(d=s.onMouseLeave)==null||d.call(s,c)},onFocus:c=>{var d;c.currentTarget.style.borderColor=p?"var(--lucent-danger-default)":"var(--lucent-focus-ring)",c.currentTarget.style.boxShadow=`0 0 0 3px ${p?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`,(d=s.onFocus)==null||d.call(s,c)},onBlur:c=>{var d;c.currentTarget.style.borderColor=p?"var(--lucent-danger-default)":"var(--lucent-border-default)",c.currentTarget.style.boxShadow="none",(d=s.onBlur)==null||d.call(s,c)},...s}),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})]}),p&&e.jsx("span",{id:`${f}-error`,role:"alert",style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:a}),!p&&t&&e.jsx("span",{id:`${f}-helper`,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)"},children:t})]})});M.displayName="Input";const oe={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"]}},N=b.forwardRef(({label:n,helperText:t,errorText:a,autoResize:r=!1,maxLength:o,showCount:i=!1,id:l,value:s,onChange:u,style:f,...p},m)=>{const c=b.useRef(null),d=m??c,h=l??`lucent-textarea-${Math.random().toString(36).slice(2,7)}`,g=!!a,y=typeof s=="string"?s.length:0;return b.useEffect(()=>{if(!r)return;const v=d.current;v&&(v.style.height="auto",v.style.height=`${v.scrollHeight}px`)},[s,r,d]),e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",width:"100%"},children:[n&&e.jsx("label",{htmlFor:h,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:d,id:h,maxLength:o,value:s,onChange:u,"aria-invalid":g,"aria-describedby":g?`${h}-error`:t?`${h}-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 ${g?"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)",...f},onFocus:v=>{var S;v.currentTarget.style.borderColor=g?"var(--lucent-danger-default)":"var(--lucent-focus-ring)",v.currentTarget.style.boxShadow=`0 0 0 3px ${g?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`,(S=p.onFocus)==null||S.call(p,v)},onBlur:v=>{var S;v.currentTarget.style.borderColor=g?"var(--lucent-danger-default)":"var(--lucent-border-default)",v.currentTarget.style.boxShadow="none",(S=p.onBlur)==null||S.call(p,v)},...p}),e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start"},children:[e.jsxs("div",{children:[g&&e.jsx("span",{id:`${h}-error`,role:"alert",style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:a}),!g&&t&&e.jsx("span",{id:`${h}-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&&y>=o?"var(--lucent-danger-text)":"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-mono)",flexShrink:0,marginLeft:"var(--lucent-space-2)"},children:[y,o?`/${o}`:""]})]})]})});N.displayName="Textarea";const ie={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"]}},se={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)"}},le={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 ce({variant:n="neutral",size:t="md",dot:a=!1,children:r,style:o}){const i=se[n],l=le[t];return e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-1)",height:l.height,padding:l.padding,fontSize:l.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 de={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."}},ue={xs:24,sm:32,md:40,lg:56,xl:80},pe={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 fe(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 me({src:n,alt:t,size:a="md",initials:r,style:o,...i}){const l=ue[a],s=fe(t,r),u={width:l,height:l,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:l,height:l,style:{...u,objectFit:"cover"},...i}):e.jsx("span",{role:"img","aria-label":t,style:{...u,background:"var(--lucent-accent-default)",color:"var(--lucent-text-on-accent)",fontSize:pe[a],fontWeight:"var(--lucent-font-weight-semibold)",fontFamily:"var(--lucent-font-family-base)"},children:s})}const he={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>.'}},ge={xs:12,sm:16,md:24,lg:36},be={xs:2.5,sm:2.5,md:2,lg:2};function B({size:n="md",label:t="Loading…",color:a}){const r=ge[n],o=be[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 ve({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 xe={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"]}},we={sm:14,md:16},Se=`
|
|
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=`
|
|
2
2
|
@keyframes lucent-cb-pop {
|
|
3
3
|
0% { transform: scale(1); }
|
|
4
4
|
35% { transform: scale(0.82); }
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
60% { transform: scale(1.15) rotate(2deg); }
|
|
11
11
|
100% { opacity: 1; transform: scale(1) rotate(0deg); }
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
`,D=g.forwardRef(({label:n,size:t="md",indeterminate:a=!1,checked:r,defaultChecked:o,disabled:i,id:s,onChange:l,style:d,...p},f)=>{const m=g.useRef(null),c=s??`lucent-checkbox-${Math.random().toString(36).slice(2,7)}`,u=Se[t],b=r!==void 0,[h,x]=g.useState(o??!1),w=b?!!r:h,v=g.useRef(w),[S,y]=g.useState(0);g.useEffect(()=>{!i&&v.current!==w&&(v.current=w,y(j=>j+1))},[w,i]);const I=g.useCallback(j=>{m.current=j,typeof f=="function"?f(j):f&&(f.current=j)},[f]);g.useEffect(()=>{m.current&&(m.current.indeterminate=a)},[a]);const T=j=>{b||x(j.target.checked),l==null||l(j)},M=i?"var(--lucent-text-disabled)":"var(--lucent-text-on-accent)",q={width:u,height:u,borderRadius:"var(--lucent-radius-sm)",border:`1.5px solid ${i?"transparent":w||a?"var(--lucent-accent-default)":"var(--lucent-border-strong)"}`,background:i?"var(--lucent-bg-muted)":w||a?"var(--lucent-accent-default)":"var(--lucent-surface-default)",display:"inline-flex",alignItems:"center",justifyContent:"center",flexShrink:0,transition:"background var(--lucent-duration-fast) var(--lucent-easing-default), border-color var(--lucent-duration-fast) var(--lucent-easing-default)",animation:S>0?"lucent-cb-pop 220ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards":void 0};return e.jsxs(e.Fragment,{children:[e.jsx("style",{children:ke}),e.jsxs("label",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-2)",cursor:i?"not-allowed":"pointer",fontFamily:"var(--lucent-font-family-base)",fontSize:t==="sm"?"var(--lucent-font-size-sm)":"var(--lucent-font-size-md)",color:i?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",userSelect:"none",...d},children:[e.jsx("input",{ref:I,type:"checkbox",id:c,checked:b?r:h,disabled:i,onChange:T,style:{position:"absolute",opacity:0,width:0,height:0,margin:0,pointerEvents:"none"},...p}),e.jsxs("span",{"aria-hidden":!0,style:q,children:[w&&!a&&e.jsx("svg",{width:u-4,height:u-4,viewBox:"0 0 10 10",fill:"none",style:{animation:"lucent-cb-mark 200ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards"},children:e.jsx("path",{d:"M1.5 5L4 7.5L8.5 2.5",stroke:M,strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round"})}),a&&e.jsx("svg",{width:u-4,height:u-4,viewBox:"0 0 10 10",fill:"none",style:{animation:"lucent-cb-mark 200ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards"},children:e.jsx("path",{d:"M2 5H8",stroke:M,strokeWidth:1.5,strokeLinecap:"round"})})]},S),n]})]})});D.displayName="Checkbox";const Te={id:"checkbox",name:"Checkbox",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A binary selection control for boolean values or multi-select lists.",designIntent:'Checkboxes represent independent boolean choices — they do not affect each other. Use a Checkbox for settings that take effect immediately (e.g. "Remember me") or for selecting multiple items from a list. When only one option may be active at a time, use Radio instead. The indeterminate state communicates a "select all" parent whose children are partially checked — never use it for a third logical state.',props:[{name:"checked",type:"boolean",required:!1,description:"Controlled checked state. Pair with onChange for controlled usage."},{name:"defaultChecked",type:"boolean",required:!1,default:"false",description:"Initial checked state for uncontrolled usage."},{name:"onChange",type:"function",required:!1,description:"Called when the checked state changes."},{name:"label",type:"string",required:!1,description:"Visible label rendered beside the checkbox."},{name:"indeterminate",type:"boolean",required:!1,default:"false",description:"Displays a dash to indicate a partially-checked parent state."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Prevents interaction and dims the control."},{name:"size",type:"enum",required:!1,default:"md",description:"Size of the checkbox box.",enumValues:["sm","md"]}],usageExamples:[{title:"Controlled",code:'<Checkbox checked={agreed} onChange={e => setAgreed(e.target.checked)} label="I agree to the terms" />'},{title:"Uncontrolled",code:'<Checkbox defaultChecked label="Send me updates" />'},{title:"Indeterminate",code:'<Checkbox indeterminate label="Select all" />'},{title:"Disabled",code:'<Checkbox disabled label="Unavailable option" />'}],compositionGraph:[],accessibility:{role:"checkbox",ariaAttributes:["aria-checked","aria-disabled"],keyboardInteractions:["Space — toggles checked state"]}},Ce=`
|
|
14
14
|
@keyframes lucent-radio-pop {
|
|
15
15
|
0% { transform: scale(1); }
|
|
16
16
|
35% { transform: scale(0.82); }
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
60% { transform: scale(1.2); }
|
|
23
23
|
100% { opacity: 1; transform: scale(1); }
|
|
24
24
|
}
|
|
25
|
-
`,
|
|
25
|
+
`,V=g.createContext(null);function W({name:n,value:t,onChange:a,disabled:r,orientation:o="vertical",label:i,children:s}){return e.jsx(V.Provider,{value:{name:n,value:t,onChange:a,disabled:r??!1},children:e.jsx("div",{role:"radiogroup","aria-label":i,style:{display:"flex",flexDirection:o==="vertical"?"column":"row",gap:o==="vertical"?"var(--lucent-space-3)":"var(--lucent-space-4)",flexWrap:"wrap"},children:s})})}const Ie={sm:14,md:16};function je({value:n,label:t,size:a="md",disabled:r,id:o,onChange:i,checked:s,...l}){const d=g.useContext(V),p=o??`lucent-radio-${Math.random().toString(36).slice(2,7)}`,f=Ie[a],m=r??(d==null?void 0:d.disabled)??!1,c=d?d.value===n:!!s,u=g.useRef(c),[b,h]=g.useState(0);g.useEffect(()=>{!m&&u.current!==c&&(u.current=c,h(S=>S+1))},[c,m]);const x=S=>{d==null||d.onChange(n),i==null||i(S)},w={width:f/2,height:f/2,borderRadius:"50%",background:m?"var(--lucent-text-disabled)":"var(--lucent-text-on-accent)",animation:c?"lucent-radio-dot 200ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards":void 0,opacity:c?1:0},v={width:f,height:f,borderRadius:"50%",border:`1.5px solid ${m?"transparent":c?"var(--lucent-accent-default)":"var(--lucent-border-strong)"}`,background:m?"var(--lucent-bg-muted)":c?"var(--lucent-accent-default)":"var(--lucent-surface-default)",display:"inline-flex",alignItems:"center",justifyContent:"center",flexShrink:0,transition:"background var(--lucent-duration-fast) var(--lucent-easing-default), border-color var(--lucent-duration-fast) var(--lucent-easing-default)",animation:b>0?"lucent-radio-pop 220ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards":void 0};return e.jsxs(e.Fragment,{children:[e.jsx("style",{children:Ce}),e.jsxs("label",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-2)",cursor:m?"not-allowed":"pointer",fontFamily:"var(--lucent-font-family-base)",fontSize:a==="sm"?"var(--lucent-font-size-sm)":"var(--lucent-font-size-md)",color:m?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",userSelect:"none"},children:[e.jsx("input",{type:"radio",id:p,value:n,name:(d==null?void 0:d.name)??l.name,checked:c,disabled:m,onChange:x,style:{position:"absolute",opacity:0,width:0,height:0,margin:0,pointerEvents:"none"},...l}),e.jsx("span",{"aria-hidden":!0,style:v,children:e.jsx("span",{style:w})},b),t]})]})}function ze({defaultValue:n="",onChange:t,...a}){const[r,o]=g.useState(n);return e.jsx(W,{...a,value:r,onChange:i=>{o(i),t==null||t(i)}})}const Me={id:"radio",name:"Radio",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A mutually exclusive selection control. Use RadioGroup to manage a set of options.",designIntent:"Radio buttons enforce a single selection from a small set of options (typically 2–6). Always wrap Radio in a RadioGroup so name and selection state are shared automatically. For larger option sets (7+) prefer a Select. For independent true/false choices, use a Checkbox. RadioGroup orientation should match how the options relate — vertical for distinct choices, horizontal for brief inline options (e.g. Yes / No).",props:[{name:"value",type:"string",required:!0,description:"The value submitted when this radio is selected."},{name:"label",type:"string",required:!1,description:"Visible label rendered beside the radio button."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Prevents interaction. Inherits group-level disabled when inside RadioGroup."},{name:"size",type:"enum",required:!1,default:"md",description:"Size of the radio button circle.",enumValues:["sm","md"]}],usageExamples:[{title:"Controlled RadioGroup",code:`
|
|
26
26
|
<RadioGroup name="plan" value={plan} onChange={setPlan}>
|
|
27
27
|
<Radio value="free" label="Free" />
|
|
28
28
|
<Radio value="pro" label="Pro" />
|
|
@@ -36,23 +36,23 @@
|
|
|
36
36
|
<RadioGroup name="tier" value="basic" onChange={() => {}} disabled>
|
|
37
37
|
<Radio value="basic" label="Basic" />
|
|
38
38
|
<Radio value="advanced" label="Advanced" />
|
|
39
|
-
</RadioGroup>`.trim()}],compositionGraph:[{componentId:"radio-group",componentName:"RadioGroup",role:"container",required:!0},{componentId:"radio",componentName:"Radio",role:"item",required:!0}],accessibility:{role:"radio",ariaAttributes:["aria-checked","aria-disabled"],keyboardInteractions:["Arrow keys — move selection within the group","Space — selects the focused radio"]}},
|
|
39
|
+
</RadioGroup>`.trim()}],compositionGraph:[{componentId:"radio-group",componentName:"RadioGroup",role:"container",required:!0},{componentId:"radio",componentName:"Radio",role:"item",required:!0}],accessibility:{role:"radio",ariaAttributes:["aria-checked","aria-disabled"],keyboardInteractions:["Arrow keys — move selection within the group","Space — selects the focused radio"]}},qe={sm:{track:[28,16],thumb:12},md:{track:[36,20],thumb:16},lg:{track:[44,24],thumb:20}},F="cubic-bezier(0.34, 1.56, 0.64, 1)",Ee=`
|
|
40
40
|
@keyframes lucent-toggle-pop {
|
|
41
41
|
0% { transform: scale(1); }
|
|
42
42
|
35% { transform: scale(0.94); }
|
|
43
43
|
70% { transform: scale(1.06); }
|
|
44
44
|
100% { transform: scale(1); }
|
|
45
45
|
}
|
|
46
|
-
`;function
|
|
46
|
+
`;function Re({label:n,size:t="md",checked:a,defaultChecked:r,disabled:o,id:i,onChange:s,style:l,...d}){const p=i??`lucent-toggle-${Math.random().toString(36).slice(2,7)}`,f=a!==void 0,[m,c]=g.useState(r??!1),u=f?!!a:m,b=g.useRef(u),[h,x]=g.useState(0);g.useEffect(()=>{!o&&b.current!==u&&(b.current=u,x(T=>T+1))},[u,o]);const{track:[w,v],thumb:S}=qe[t],y=u?w-S-2:2,I=T=>{f||c(T.target.checked),s==null||s(T)};return e.jsxs(e.Fragment,{children:[e.jsx("style",{children:Ee}),e.jsxs("label",{style:{display:"inline-flex",alignItems:"center",gap:"var(--lucent-space-2)",cursor:o?"not-allowed":"pointer",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-md)",color:o?"var(--lucent-text-disabled)":"var(--lucent-text-primary)",userSelect:"none",...l},children:[e.jsx("input",{type:"checkbox",role:"switch",id:p,checked:f?a:m,disabled:o,onChange:I,"aria-checked":u,style:{position:"absolute",opacity:0,width:0,height:0,margin:0,pointerEvents:"none"},...d}),e.jsx("span",{"aria-hidden":!0,style:{position:"relative",width:w,height:v,borderRadius:v/2,background:o?"var(--lucent-bg-muted)":u?"var(--lucent-accent-default)":"var(--lucent-border-strong)",flexShrink:0,transition:"background var(--lucent-duration-fast) var(--lucent-easing-default)",animation:h>0?`lucent-toggle-pop 240ms ${F} forwards`:void 0},children:e.jsx("span",{style:{position:"absolute",top:2,left:y,width:S,height:S,borderRadius:"50%",background:"#ffffff",boxShadow:"0 1px 3px rgb(0 0 0 / 0.2)",transition:`left 260ms ${F}`}})},h),n]})]})}const Fe={id:"toggle",name:"Toggle",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A sliding switch for immediately-applied binary settings.",designIntent:'Toggles are for settings that take effect the moment they are flipped — no Save button needed. If the action requires a confirmation step or only applies on form submit, use a Checkbox instead. The "on" state is visually represented by the accent colour so the meaning is clear without relying on text alone. Keep the label short (2–4 words) and phrase it as the enabled state (e.g. "Dark mode", not "Enable dark mode").',props:[{name:"checked",type:"boolean",required:!1,description:"Controlled on/off state. Pair with onChange for controlled usage."},{name:"defaultChecked",type:"boolean",required:!1,default:"false",description:"Initial state for uncontrolled usage."},{name:"onChange",type:"function",required:!1,description:"Called when the toggle is flipped."},{name:"label",type:"string",required:!1,description:"Visible label rendered beside the toggle."},{name:"size",type:"enum",required:!1,default:"md",description:"Controls the track and thumb size.",enumValues:["sm","md","lg"]},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Prevents interaction and dims the control."}],usageExamples:[{title:"Controlled",code:'<Toggle checked={darkMode} onChange={e => setDarkMode(e.target.checked)} label="Dark mode" />'},{title:"Uncontrolled",code:'<Toggle defaultChecked label="Email notifications" />'},{title:"Sizes",code:`<Toggle size="sm" label="Compact" />
|
|
47
47
|
<Toggle size="md" label="Default" />
|
|
48
|
-
<Toggle size="lg" label="Large" />`},{title:"Disabled",code:'<Toggle disabled label="Unavailable setting" />'}],compositionGraph:[],accessibility:{role:"switch",ariaAttributes:["aria-checked","aria-disabled"],keyboardInteractions:["Space — toggles the switch state"]}},
|
|
48
|
+
<Toggle size="lg" label="Large" />`},{title:"Disabled",code:'<Toggle disabled label="Unavailable setting" />'}],compositionGraph:[],accessibility:{role:"switch",ariaAttributes:["aria-checked","aria-disabled"],keyboardInteractions:["Space — toggles the switch state"]}},Ae={sm:"32px",md:"40px",lg:"46px"},Ne={sm:"var(--lucent-font-size-sm)",md:"var(--lucent-font-size-md)",lg:"var(--lucent-font-size-lg)"},P=g.forwardRef(({options:n,size:t="md",label:a,helperText:r,errorText:o,placeholder:i,disabled:s,id:l,style:d,...p},f)=>{const m=l??`lucent-select-${Math.random().toString(36).slice(2,7)}`,c=!!o;return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)",width:"100%"},children:[a&&e.jsx("label",{htmlFor:m,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:a}),e.jsxs("div",{style:{position:"relative",display:"flex",alignItems:"center"},children:[e.jsxs("select",{ref:f,id:m,disabled:s,"aria-invalid":c,"aria-describedby":c?`${m}-error`:r?`${m}-helper`:void 0,style:{width:"100%",height:Ae[t],padding:"0 var(--lucent-space-8) 0 var(--lucent-space-3)",fontSize:Ne[t],fontFamily:"var(--lucent-font-family-base)",color:"var(--lucent-text-primary)",background:"var(--lucent-surface-default)",border:`1px solid ${c?"var(--lucent-danger-default)":"var(--lucent-border-default)"}`,borderRadius:"var(--lucent-radius-lg)",outline:"none",boxSizing:"border-box",appearance:"none",cursor:s?"not-allowed":"pointer",transition:"border-color var(--lucent-duration-fast) var(--lucent-easing-default)",...d},onMouseEnter:u=>{var b;!s&&u.currentTarget!==document.activeElement&&(u.currentTarget.style.borderColor=c?"var(--lucent-danger-default)":"var(--lucent-border-strong)"),(b=p.onMouseEnter)==null||b.call(p,u)},onMouseLeave:u=>{var b;!s&&u.currentTarget!==document.activeElement&&(u.currentTarget.style.borderColor=c?"var(--lucent-danger-default)":"var(--lucent-border-default)"),(b=p.onMouseLeave)==null||b.call(p,u)},onFocus:u=>{var b;u.currentTarget.style.borderColor=c?"var(--lucent-danger-default)":"var(--lucent-focus-ring)",u.currentTarget.style.boxShadow=`0 0 0 3px ${c?"var(--lucent-danger-subtle)":"var(--lucent-accent-subtle)"}`,(b=p.onFocus)==null||b.call(p,u)},onBlur:u=>{var b;u.currentTarget.style.borderColor=c?"var(--lucent-danger-default)":"var(--lucent-border-default)",u.currentTarget.style.boxShadow="none",(b=p.onBlur)==null||b.call(p,u)},...p,children:[i&&e.jsx("option",{value:"",disabled:!0,children:i}),n.map(u=>e.jsx("option",{value:u.value,disabled:u.disabled,children:u.label},u.value))]}),e.jsx("span",{"aria-hidden":!0,style:{position:"absolute",right:"var(--lucent-space-3)",pointerEvents:"none",color:"var(--lucent-text-secondary)",display:"flex",alignItems:"center"},children:e.jsx("svg",{width:14,height:14,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("polyline",{points:"6 9 12 15 18 9"})})})]}),c&&e.jsx("span",{id:`${m}-error`,role:"alert",style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-danger-text)",fontFamily:"var(--lucent-font-family-base)"},children:o}),!c&&r&&e.jsx("span",{id:`${m}-helper`,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)"},children:r})]})});P.displayName="Select";const Be={id:"select",name:"Select",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A dropdown for choosing one option from a list.",designIntent:"Select is best for 5–15 options where showing all at once would be too noisy. For fewer than 5 options, prefer RadioGroup (always-visible, faster to scan). For search-filtered or async options, a Combobox (Wave 3) is more appropriate. Always provide a placeholder when no default is sensible — this communicates the field is required and prevents silent zero-state submissions.",props:[{name:"options",type:"array",required:!0,description:"Array of { value, label, disabled? } option objects."},{name:"value",type:"string",required:!1,description:"Controlled selected value. Pair with onChange."},{name:"onChange",type:"function",required:!1,description:"Called when the selected value changes."},{name:"placeholder",type:"string",required:!1,description:"Disabled first option shown when no value is selected."},{name:"label",type:"string",required:!1,description:"Visible label rendered above the select."},{name:"helperText",type:"string",required:!1,description:"Supplementary hint shown below the select."},{name:"errorText",type:"string",required:!1,description:"Validation error message. Replaces helperText and applies error styling."},{name:"size",type:"enum",required:!1,default:"md",description:"Controls the height of the select control.",enumValues:["sm","md","lg"]},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Prevents interaction."}],usageExamples:[{title:"Controlled",code:`
|
|
49
49
|
<Select
|
|
50
50
|
label="Country"
|
|
51
51
|
placeholder="Choose a country"
|
|
52
52
|
options={countries}
|
|
53
53
|
value={country}
|
|
54
54
|
onChange={e => setCountry(e.target.value)}
|
|
55
|
-
/>`.trim()},{title:"With validation error",code:'<Select label="Role" options={roles} errorText="Please select a role" />'},{title:"With helper text",code:'<Select label="Timezone" options={timezones} helperText="Used for scheduling notifications" />'}],compositionGraph:[],accessibility:{role:"combobox",ariaAttributes:["aria-invalid","aria-describedby"],keyboardInteractions:["Enter / Space — opens the option list","Arrow keys — navigate options","Escape — closes the list"]}},Be={neutral:{bg:"var(--lucent-bg-muted)",color:"var(--lucent-text-secondary)",border:"var(--lucent-border-default)",dismissHover:"var(--lucent-border-strong)"},accent:{bg:"var(--lucent-accent-subtle)",color:"var(--lucent-accent-active)",border:"var(--lucent-accent-subtle)",dismissHover:"var(--lucent-accent-default)"},success:{bg:"var(--lucent-success-subtle)",color:"var(--lucent-success-text)",border:"var(--lucent-success-subtle)",dismissHover:"var(--lucent-success-default)"},warning:{bg:"var(--lucent-warning-subtle)",color:"var(--lucent-warning-text)",border:"var(--lucent-warning-subtle)",dismissHover:"var(--lucent-warning-default)"},danger:{bg:"var(--lucent-danger-subtle)",color:"var(--lucent-danger-text)",border:"var(--lucent-danger-subtle)",dismissHover:"var(--lucent-danger-default)"},info:{bg:"var(--lucent-info-subtle)",color:"var(--lucent-info-text)",border:"var(--lucent-info-subtle)",dismissHover:"var(--lucent-info-default)"}},$e={sm:{fontSize:"var(--lucent-font-size-xs)",height:"20px",padding:"0 var(--lucent-space-2)",iconSize:10,gap:"var(--lucent-space-1)"},md:{fontSize:"var(--lucent-font-size-sm)",height:"24px",padding:"0 var(--lucent-space-2)",iconSize:12,gap:"var(--lucent-space-1)"}};function De({children:n,variant:t="neutral",size:a="md",onDismiss:r,disabled:o}){const i=Be[t],l=$e[a];return e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:l.gap,height:l.height,padding:r?"0 var(--lucent-space-1) 0 var(--lucent-space-2)":l.padding,fontSize:l.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",opacity:o?.5:1},children:[n,r&&e.jsx("button",{type:"button",onClick:o?void 0:r,disabled:o,"aria-label":"Dismiss",style:{display:"inline-flex",alignItems:"center",justifyContent:"center",width:l.iconSize+4,height:l.iconSize+4,padding:0,border:"none",borderRadius:"var(--lucent-radius-full)",background:"transparent",color:"inherit",cursor:o?"not-allowed":"pointer",flexShrink:0,lineHeight:1},onMouseEnter:s=>{o||(s.currentTarget.style.background=i.dismissHover+"33")},onMouseLeave:s=>{s.currentTarget.style.background="transparent"},children:e.jsx("svg",{width:l.iconSize,height:l.iconSize,viewBox:"0 0 10 10",fill:"none",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"round",children:e.jsx("path",{d:"M2 2L8 8M8 2L2 8"})})})]})}const Le={id:"tag",name:"Tag",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A dismissible label for applied filters, selected values, or categorisation.",designIntent:"Tags represent user-applied selections that can be removed — filters, multi-select values, categories. They differ from Badge in that they are interactive: provide onDismiss when the user should be able to remove the tag. Without onDismiss they render as a static pill (identical to Badge in purpose). Use semantic variants to match meaning; default to neutral for user-generated content where no semantic applies.",props:[{name:"children",type:"ReactNode",required:!0,description:"Tag label content."},{name:"variant",type:"enum",required:!1,default:"neutral",description:"Colour scheme conveying semantic meaning.",enumValues:["neutral","accent","success","warning","danger","info"]},{name:"size",type:"enum",required:!1,default:"md",description:"Controls height and font size.",enumValues:["sm","md"]},{name:"onDismiss",type:"function",required:!1,description:"When provided, renders an × button that calls this handler on click."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Dims the tag and prevents the dismiss action."}],usageExamples:[{title:"Dismissible filter",code:"<Tag onDismiss={() => removeFilter('react')}>React</Tag>"},{title:"Multi-select value",code:'<Tag variant="accent" onDismiss={() => deselect(id)}>Jane Doe</Tag>'},{title:"Static category",code:'<Tag variant="info">Beta</Tag>'},{title:"Danger (removable)",code:'<Tag variant="danger" onDismiss={handleRemove}>Blocked</Tag>'}],compositionGraph:[],accessibility:{role:"group",notes:'The dismiss button has aria-label="Dismiss" and is keyboard-focusable.',keyboardInteractions:["Enter / Space — activates the dismiss button when focused"]}},x=5,Ve={top:{bottom:"100%",left:"50%",transform:"translateX(-50%)",marginBottom:x+3},bottom:{top:"100%",left:"50%",transform:"translateX(-50%)",marginTop:x+3},left:{right:"100%",top:"50%",transform:"translateY(-50%)",marginRight:x+3},right:{left:"100%",top:"50%",transform:"translateY(-50%)",marginLeft:x+3}},Pe={top:{bottom:-x,left:"50%",transform:"translateX(-50%)",borderWidth:`${x}px ${x}px 0 ${x}px`,borderColor:"var(--lucent-text-primary) transparent transparent transparent"},bottom:{top:-x,left:"50%",transform:"translateX(-50%)",borderWidth:`0 ${x}px ${x}px ${x}px`,borderColor:"transparent transparent var(--lucent-text-primary) transparent"},left:{right:-x,top:"50%",transform:"translateY(-50%)",borderWidth:`${x}px 0 ${x}px ${x}px`,borderColor:"transparent transparent transparent var(--lucent-text-primary)"},right:{left:-x,top:"50%",transform:"translateY(-50%)",borderWidth:`${x}px ${x}px ${x}px 0`,borderColor:"transparent var(--lucent-text-primary) transparent transparent"}};function We({content:n,children:t,placement:a="top",delay:r=300}){const[o,i]=b.useState(!1),l=b.useRef(null),s=()=>{l.current=setTimeout(()=>i(!0),r)},u=()=>{l.current&&clearTimeout(l.current),i(!1)};return n?e.jsxs("span",{style:{position:"relative",display:"inline-flex"},onMouseEnter:s,onMouseLeave:u,onFocus:s,onBlur:u,children:[t,o&&e.jsxs("span",{role:"tooltip",style:{position:"absolute",...Ve[a],background:"var(--lucent-text-primary)",color:"var(--lucent-bg-base)",padding:"5px 10px",borderRadius:"var(--lucent-radius-md)",fontSize:"var(--lucent-font-size-xs)",fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-medium)",lineHeight:"var(--lucent-line-height-base)",whiteSpace:"nowrap",zIndex:9999,pointerEvents:"none",boxShadow:"var(--lucent-shadow-md)"},children:[n,e.jsx("span",{"aria-hidden":!0,style:{position:"absolute",width:0,height:0,borderStyle:"solid",...Pe[a]}})]})]}):e.jsx(e.Fragment,{children:t})}const Oe={id:"tooltip",name:"Tooltip",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A transient label that appears on hover or focus to explain an element.",designIntent:'Tooltips surface supplementary context that would clutter the UI if shown permanently — keyboard shortcut hints, icon button labels, truncated text expansions. They must never contain critical information (errors, required actions) because they are invisible until hovered. Keep content to one short phrase; avoid wrapping. Use placement to avoid viewport edges — "top" is the default and works in most cases.',props:[{name:"content",type:"ReactNode",required:!0,description:"Text or element shown inside the tooltip. Keep it short (under 80 chars)."},{name:"children",type:"ReactNode",required:!0,description:"The element that triggers the tooltip on hover/focus."},{name:"placement",type:"enum",required:!1,default:"top",description:"Position of the tooltip relative to the trigger.",enumValues:["top","bottom","left","right"]},{name:"delay",type:"number",required:!1,default:"300",description:"Milliseconds before the tooltip appears. Prevents flicker on fast cursor movement."}],usageExamples:[{title:"Icon button label",code:'<Tooltip content="Copy to clipboard"><IconButton icon={<CopyIcon />} /></Tooltip>'},{title:"Keyboard shortcut hint",code:'<Tooltip content="⌘K"><Button variant="ghost">Search</Button></Tooltip>'},{title:"Bottom placement",code:'<Tooltip content="Opens in a new tab" placement="bottom"><a href="…">Link</a></Tooltip>'},{title:"No delay (instant)",code:'<Tooltip content="Delete" delay={0}><Button variant="ghost">🗑</Button></Tooltip>'}],compositionGraph:[],accessibility:{role:"tooltip",ariaAttributes:['role="tooltip"'],notes:'The tooltip is shown on both hover and focus, making it accessible to keyboard users. Content is exposed via role="tooltip".'}},He={xs:12,sm:14,md:16,lg:20,xl:24};function Ge({children:n,size:t="md",label:a,color:r,style:o}){const i=He[t];return e.jsx("span",{role:a?"img":void 0,"aria-label":a,"aria-hidden":a?void 0:!0,style:{display:"inline-flex",alignItems:"center",justifyContent:"center",width:i,height:i,flexShrink:0,color:r??"currentColor",...o},children:n})}const Ue={id:"icon",name:"Icon",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A sized, accessible wrapper for any SVG or icon-set component.",designIntent:"Icon is intentionally icon-set agnostic — pass any SVG or third-party icon as children and it handles sizing and accessibility automatically. Use label for standalone icons that convey meaning without adjacent text (e.g. a close button). Omit label for decorative icons next to text, where the text already provides context. colour inherits from the parent by default via currentColor — override only when the icon must differ from its surrounding text.",props:[{name:"children",type:"ReactNode",required:!0,description:"The icon to render — any SVG element or icon-set component."},{name:"size",type:"enum",required:!1,default:"md",description:"Constrains the icon to a fixed square (xs=12, sm=14, md=16, lg=20, xl=24).",enumValues:["xs","sm","md","lg","xl"]},{name:"label",type:"string",required:!1,description:"Accessible label (aria-label). Provide for meaningful standalone icons; omit for decorative ones."},{name:"color",type:"string",required:!1,description:"CSS colour value. Defaults to currentColor (inherits from parent)."}],usageExamples:[{title:"Decorative (next to text)",code:'<Icon size="md"><SearchIcon /></Icon>'},{title:"Meaningful (standalone)",code:'<Icon size="lg" label="Close dialog"><XIcon /></Icon>'},{title:"Coloured",code:'<Icon color="var(--lucent-danger-default)"><AlertIcon /></Icon>'},{title:"Inside a button",code:'<Button leftIcon={<Icon size="sm"><PlusIcon /></Icon>}>Add item</Button>'}],compositionGraph:[],accessibility:{role:"img",ariaAttributes:["aria-label","aria-hidden"],notes:'aria-hidden="true" is applied automatically when no label is given, hiding the icon from screen readers.'}},_e={primary:"var(--lucent-text-primary)",secondary:"var(--lucent-text-secondary)",disabled:"var(--lucent-text-disabled)",inverse:"var(--lucent-text-inverse)",onAccent:"var(--lucent-text-on-accent)",success:"var(--lucent-success-text)",warning:"var(--lucent-warning-text)",danger:"var(--lucent-danger-text)",info:"var(--lucent-info-text)"},Ke={xs:"var(--lucent-font-size-xs)",sm:"var(--lucent-font-size-sm)",md:"var(--lucent-font-size-md)",lg:"var(--lucent-font-size-lg)",xl:"var(--lucent-font-size-xl)","2xl":"var(--lucent-font-size-2xl)","3xl":"var(--lucent-font-size-3xl)"},Xe={regular:"var(--lucent-font-weight-regular)",medium:"var(--lucent-font-weight-medium)",semibold:"var(--lucent-font-weight-semibold)",bold:"var(--lucent-font-weight-bold)"},Ye={tight:"var(--lucent-line-height-tight)",base:"var(--lucent-line-height-base)",relaxed:"var(--lucent-line-height-relaxed)"},Je={base:"var(--lucent-font-family-base)",mono:"var(--lucent-font-family-mono)",display:"var(--lucent-font-family-display)"};function C({as:n="p",size:t="md",weight:a="regular",color:r="primary",align:o="left",lineHeight:i="base",family:l="base",truncate:s=!1,children:u,style:f,...p}){const m={fontSize:Ke[t],fontWeight:Xe[a],color:_e[r],textAlign:o,lineHeight:Ye[i],fontFamily:Je[l],margin:0,...s&&{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},...f},c=n;return e.jsx(c,{style:m,...p,children:u})}const Qe={id:"text",name:"Text",tier:"atom",domain:"neutral",specVersion:"0.1",description:"Polymorphic typography primitive that maps semantic HTML elements to design-system type scales.",designIntent:"Text is the single source of truth for typography in Lucent UI. Rather than hard-coding font sizes and colors directly in components, every piece of rendered text should pass through this atom. The `as` prop controls the HTML element (and therefore semantic meaning), while `size`, `weight`, `color`, and `lineHeight` control appearance independently — decoupling semantics from style. Use `truncate` only in constrained-width containers where overflow must be prevented; it applies overflow: hidden + text-overflow: ellipsis. The `family` prop enables inline code or monospaced content without switching components.",props:[{name:"as",type:"enum",required:!1,default:"p",description:"HTML element to render. Controls semantic meaning independently of visual style.",enumValues:["p","h1","h2","h3","h4","h5","h6","span","div","label","strong","em","code"]},{name:"size",type:"enum",required:!1,default:"md",description:"Font size from the type scale. Maps to the corresponding --lucent-font-size-* token.",enumValues:["xs","sm","md","lg","xl","2xl","3xl"]},{name:"weight",type:"enum",required:!1,default:"regular",description:"Font weight. Maps to --lucent-font-weight-* tokens.",enumValues:["regular","medium","semibold","bold"]},{name:"color",type:"enum",required:!1,default:"primary",description:"Semantic text color. All values adapt automatically in light and dark themes.",enumValues:["primary","secondary","disabled","inverse","onAccent","success","warning","danger","info"]},{name:"align",type:"enum",required:!1,default:"left",description:"Text alignment.",enumValues:["left","center","right"]},{name:"lineHeight",type:"enum",required:!1,default:"base",description:"Line height. tight=1.25, base=1.5, relaxed=1.75.",enumValues:["tight","base","relaxed"]},{name:"family",type:"enum",required:!1,default:"base",description:"Font family. Use mono for code or tabular data. Use display (Unbounded) for headings and marketing copy.",enumValues:["base","mono","display"]},{name:"truncate",type:"boolean",required:!1,default:"false",description:"Clips overflow text with an ellipsis. Requires the container to have a constrained width."},{name:"children",type:"ReactNode",required:!0,description:"The text content to render."},{name:"style",type:"object",required:!1,description:"Inline style overrides. Applied after computed token styles."}],usageExamples:[{title:"Heading",code:'<Text as="h1" size="3xl" weight="bold">Page title</Text>'},{title:"Body paragraph",code:'<Text size="md" color="secondary">Supporting copy goes here.</Text>'},{title:"Label",code:'<Text as="label" size="sm" weight="medium" htmlFor="email">Email</Text>'},{title:"Inline code",code:'<Text as="code" family="mono" size="sm">const x = 1;</Text>'},{title:"Truncated",code:"<Text truncate style={{ maxWidth: 200 }}>Very long text that will be clipped</Text>"},{title:"Status color",code:'<Text color="danger" size="xs">This field is required</Text>'}],compositionGraph:[],accessibility:{notes:'The rendered element determines the implicit ARIA role. Use heading elements (h1–h6) for document headings so screen readers can navigate the page structure. Use `as="label"` with `htmlFor` to associate labels with form controls. Decorative text needs no additional ARIA.'}};function Ze({label:n,htmlFor:t,required:a=!1,helperText:r,errorMessage:o,children:i,style:l}){const s=o??r,u=o?"danger":"secondary";return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-2)",...l},children:[n&&e.jsxs("div",{style:{display:"flex",alignItems:"baseline",gap:"var(--lucent-space-1)"},children:[e.jsx(C,{as:"label",size:"sm",weight:"medium",lineHeight:"tight",...t!==void 0&&{htmlFor:t},children:n}),a&&e.jsx(C,{as:"span",size:"sm",color:"danger",lineHeight:"tight","aria-hidden":"true",children:"*"})]}),i,s&&e.jsx(C,{size:"xs",color:u,lineHeight:"tight",children:s})]})}const et={id:"form-field",name:"FormField",tier:"molecule",domain:"neutral",specVersion:"0.1",description:"Wraps any form control (Input, Select, Textarea) with a label, helper text, and validation message.",designIntent:"FormField standardises the vertical rhythm around form controls. A label is linked to the control via htmlFor so screen readers announce it correctly. The required asterisk is decorative (aria-hidden) because the actual required state should be communicated on the input via aria-required. Helper text provides proactive guidance; errorMessage replaces it when validation fails, using danger color to draw attention. The gap between elements uses space-2 to create a tight but breathable stack.",props:[{name:"label",type:"string",required:!1,description:"Label text rendered above the control as a <label> element."},{name:"htmlFor",type:"string",required:!1,description:"ID of the form control this label describes. Forwarded to the label htmlFor attribute."},{name:"required",type:"boolean",required:!1,default:"false",description:"Appends a danger-colored asterisk after the label text."},{name:"helperText",type:"string",required:!1,description:"Secondary text below the control providing guidance. Hidden when errorMessage is set."},{name:"errorMessage",type:"string",required:!1,description:"Validation error shown in danger color below the control. Replaces helperText when set."},{name:"children",type:"ReactNode",required:!0,description:"The form control to wrap — typically Input, Select, or Textarea."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Basic field",code:`<FormField label="Email" htmlFor="email">
|
|
55
|
+
/>`.trim()},{title:"With validation error",code:'<Select label="Role" options={roles} errorText="Please select a role" />'},{title:"With helper text",code:'<Select label="Timezone" options={timezones} helperText="Used for scheduling notifications" />'}],compositionGraph:[],accessibility:{role:"combobox",ariaAttributes:["aria-invalid","aria-describedby"],keyboardInteractions:["Enter / Space — opens the option list","Arrow keys — navigate options","Escape — closes the list"]}},$e={neutral:{bg:"var(--lucent-bg-muted)",color:"var(--lucent-text-secondary)",border:"var(--lucent-border-default)",dismissHover:"var(--lucent-border-strong)"},accent:{bg:"var(--lucent-accent-subtle)",color:"var(--lucent-accent-active)",border:"var(--lucent-accent-subtle)",dismissHover:"var(--lucent-accent-default)"},success:{bg:"var(--lucent-success-subtle)",color:"var(--lucent-success-text)",border:"var(--lucent-success-subtle)",dismissHover:"var(--lucent-success-default)"},warning:{bg:"var(--lucent-warning-subtle)",color:"var(--lucent-warning-text)",border:"var(--lucent-warning-subtle)",dismissHover:"var(--lucent-warning-default)"},danger:{bg:"var(--lucent-danger-subtle)",color:"var(--lucent-danger-text)",border:"var(--lucent-danger-subtle)",dismissHover:"var(--lucent-danger-default)"},info:{bg:"var(--lucent-info-subtle)",color:"var(--lucent-info-text)",border:"var(--lucent-info-subtle)",dismissHover:"var(--lucent-info-default)"}},Le={sm:{fontSize:"var(--lucent-font-size-xs)",height:"20px",padding:"0 var(--lucent-space-2)",iconSize:10,gap:"var(--lucent-space-1)"},md:{fontSize:"var(--lucent-font-size-sm)",height:"24px",padding:"0 var(--lucent-space-2)",iconSize:12,gap:"var(--lucent-space-1)"}};function De({children:n,variant:t="neutral",size:a="md",onDismiss:r,disabled:o}){const i=$e[t],s=Le[a];return e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:s.gap,height:s.height,padding:r?"0 var(--lucent-space-1) 0 var(--lucent-space-2)":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",opacity:o?.5:1},children:[n,r&&e.jsx("button",{type:"button",onClick:o?void 0:r,disabled:o,"aria-label":"Dismiss",style:{display:"inline-flex",alignItems:"center",justifyContent:"center",width:s.iconSize+4,height:s.iconSize+4,padding:0,border:"none",borderRadius:"var(--lucent-radius-full)",background:"transparent",color:"inherit",cursor:o?"not-allowed":"pointer",flexShrink:0,lineHeight:1},onMouseEnter:l=>{o||(l.currentTarget.style.background=i.dismissHover+"33")},onMouseLeave:l=>{l.currentTarget.style.background="transparent"},children:e.jsx("svg",{width:s.iconSize,height:s.iconSize,viewBox:"0 0 10 10",fill:"none",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"round",children:e.jsx("path",{d:"M2 2L8 8M8 2L2 8"})})})]})}const Ve={id:"tag",name:"Tag",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A dismissible label for applied filters, selected values, or categorisation.",designIntent:"Tags represent user-applied selections that can be removed — filters, multi-select values, categories. They differ from Badge in that they are interactive: provide onDismiss when the user should be able to remove the tag. Without onDismiss they render as a static pill (identical to Badge in purpose). Use semantic variants to match meaning; default to neutral for user-generated content where no semantic applies.",props:[{name:"children",type:"ReactNode",required:!0,description:"Tag label content."},{name:"variant",type:"enum",required:!1,default:"neutral",description:"Colour scheme conveying semantic meaning.",enumValues:["neutral","accent","success","warning","danger","info"]},{name:"size",type:"enum",required:!1,default:"md",description:"Controls height and font size.",enumValues:["sm","md"]},{name:"onDismiss",type:"function",required:!1,description:"When provided, renders an × button that calls this handler on click."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Dims the tag and prevents the dismiss action."}],usageExamples:[{title:"Dismissible filter",code:"<Tag onDismiss={() => removeFilter('react')}>React</Tag>"},{title:"Multi-select value",code:'<Tag variant="accent" onDismiss={() => deselect(id)}>Jane Doe</Tag>'},{title:"Static category",code:'<Tag variant="info">Beta</Tag>'},{title:"Danger (removable)",code:'<Tag variant="danger" onDismiss={handleRemove}>Blocked</Tag>'}],compositionGraph:[],accessibility:{role:"group",notes:'The dismiss button has aria-label="Dismiss" and is keyboard-focusable.',keyboardInteractions:["Enter / Space — activates the dismiss button when focused"]}},k=5,We={top:{bottom:"100%",left:"50%",transform:"translateX(-50%)",marginBottom:k+3},bottom:{top:"100%",left:"50%",transform:"translateX(-50%)",marginTop:k+3},left:{right:"100%",top:"50%",transform:"translateY(-50%)",marginRight:k+3},right:{left:"100%",top:"50%",transform:"translateY(-50%)",marginLeft:k+3}},Pe={top:{bottom:-k,left:"50%",transform:"translateX(-50%)",borderWidth:`${k}px ${k}px 0 ${k}px`,borderColor:"var(--lucent-text-primary) transparent transparent transparent"},bottom:{top:-k,left:"50%",transform:"translateX(-50%)",borderWidth:`0 ${k}px ${k}px ${k}px`,borderColor:"transparent transparent var(--lucent-text-primary) transparent"},left:{right:-k,top:"50%",transform:"translateY(-50%)",borderWidth:`${k}px 0 ${k}px ${k}px`,borderColor:"transparent transparent transparent var(--lucent-text-primary)"},right:{left:-k,top:"50%",transform:"translateY(-50%)",borderWidth:`${k}px ${k}px ${k}px 0`,borderColor:"transparent var(--lucent-text-primary) transparent transparent"}};function Oe({content:n,children:t,placement:a="top",delay:r=300}){const[o,i]=g.useState(!1),s=g.useRef(null),l=()=>{s.current=setTimeout(()=>i(!0),r)},d=()=>{s.current&&clearTimeout(s.current),i(!1)};return n?e.jsxs("span",{style:{position:"relative",display:"inline-flex"},onMouseEnter:l,onMouseLeave:d,onFocus:l,onBlur:d,children:[t,o&&e.jsxs("span",{role:"tooltip",style:{position:"absolute",...We[a],background:"var(--lucent-text-primary)",color:"var(--lucent-bg-base)",padding:"5px 10px",borderRadius:"var(--lucent-radius-md)",fontSize:"var(--lucent-font-size-xs)",fontFamily:"var(--lucent-font-family-base)",fontWeight:"var(--lucent-font-weight-medium)",lineHeight:"var(--lucent-line-height-base)",whiteSpace:"nowrap",zIndex:9999,pointerEvents:"none",boxShadow:"var(--lucent-shadow-md)"},children:[n,e.jsx("span",{"aria-hidden":!0,style:{position:"absolute",width:0,height:0,borderStyle:"solid",...Pe[a]}})]})]}):e.jsx(e.Fragment,{children:t})}const He={id:"tooltip",name:"Tooltip",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A transient label that appears on hover or focus to explain an element.",designIntent:'Tooltips surface supplementary context that would clutter the UI if shown permanently — keyboard shortcut hints, icon button labels, truncated text expansions. They must never contain critical information (errors, required actions) because they are invisible until hovered. Keep content to one short phrase; avoid wrapping. Use placement to avoid viewport edges — "top" is the default and works in most cases.',props:[{name:"content",type:"ReactNode",required:!0,description:"Text or element shown inside the tooltip. Keep it short (under 80 chars)."},{name:"children",type:"ReactNode",required:!0,description:"The element that triggers the tooltip on hover/focus."},{name:"placement",type:"enum",required:!1,default:"top",description:"Position of the tooltip relative to the trigger.",enumValues:["top","bottom","left","right"]},{name:"delay",type:"number",required:!1,default:"300",description:"Milliseconds before the tooltip appears. Prevents flicker on fast cursor movement."}],usageExamples:[{title:"Icon button label",code:'<Tooltip content="Copy to clipboard"><IconButton icon={<CopyIcon />} /></Tooltip>'},{title:"Keyboard shortcut hint",code:'<Tooltip content="⌘K"><Button variant="ghost">Search</Button></Tooltip>'},{title:"Bottom placement",code:'<Tooltip content="Opens in a new tab" placement="bottom"><a href="…">Link</a></Tooltip>'},{title:"No delay (instant)",code:'<Tooltip content="Delete" delay={0}><Button variant="ghost">🗑</Button></Tooltip>'}],compositionGraph:[],accessibility:{role:"tooltip",ariaAttributes:['role="tooltip"'],notes:'The tooltip is shown on both hover and focus, making it accessible to keyboard users. Content is exposed via role="tooltip".'}},Ge={xs:12,sm:14,md:16,lg:20,xl:24};function Ue({children:n,size:t="md",label:a,color:r,style:o}){const i=Ge[t];return e.jsx("span",{role:a?"img":void 0,"aria-label":a,"aria-hidden":a?void 0:!0,style:{display:"inline-flex",alignItems:"center",justifyContent:"center",width:i,height:i,flexShrink:0,color:r??"currentColor",...o},children:n})}const _e={id:"icon",name:"Icon",tier:"atom",domain:"neutral",specVersion:"0.1",description:"A sized, accessible wrapper for any SVG or icon-set component.",designIntent:"Icon is intentionally icon-set agnostic — pass any SVG or third-party icon as children and it handles sizing and accessibility automatically. Use label for standalone icons that convey meaning without adjacent text (e.g. a close button). Omit label for decorative icons next to text, where the text already provides context. colour inherits from the parent by default via currentColor — override only when the icon must differ from its surrounding text.",props:[{name:"children",type:"ReactNode",required:!0,description:"The icon to render — any SVG element or icon-set component."},{name:"size",type:"enum",required:!1,default:"md",description:"Constrains the icon to a fixed square (xs=12, sm=14, md=16, lg=20, xl=24).",enumValues:["xs","sm","md","lg","xl"]},{name:"label",type:"string",required:!1,description:"Accessible label (aria-label). Provide for meaningful standalone icons; omit for decorative ones."},{name:"color",type:"string",required:!1,description:"CSS colour value. Defaults to currentColor (inherits from parent)."}],usageExamples:[{title:"Decorative (next to text)",code:'<Icon size="md"><SearchIcon /></Icon>'},{title:"Meaningful (standalone)",code:'<Icon size="lg" label="Close dialog"><XIcon /></Icon>'},{title:"Coloured",code:'<Icon color="var(--lucent-danger-default)"><AlertIcon /></Icon>'},{title:"Inside a button",code:'<Button leftIcon={<Icon size="sm"><PlusIcon /></Icon>}>Add item</Button>'}],compositionGraph:[],accessibility:{role:"img",ariaAttributes:["aria-label","aria-hidden"],notes:'aria-hidden="true" is applied automatically when no label is given, hiding the icon from screen readers.'}},Ke={primary:"var(--lucent-text-primary)",secondary:"var(--lucent-text-secondary)",disabled:"var(--lucent-text-disabled)",inverse:"var(--lucent-text-inverse)",onAccent:"var(--lucent-text-on-accent)",success:"var(--lucent-success-text)",warning:"var(--lucent-warning-text)",danger:"var(--lucent-danger-text)",info:"var(--lucent-info-text)"},Ye={xs:"var(--lucent-font-size-xs)",sm:"var(--lucent-font-size-sm)",md:"var(--lucent-font-size-md)",lg:"var(--lucent-font-size-lg)",xl:"var(--lucent-font-size-xl)","2xl":"var(--lucent-font-size-2xl)","3xl":"var(--lucent-font-size-3xl)"},Xe={regular:"var(--lucent-font-weight-regular)",medium:"var(--lucent-font-weight-medium)",semibold:"var(--lucent-font-weight-semibold)",bold:"var(--lucent-font-weight-bold)"},Je={tight:"var(--lucent-line-height-tight)",base:"var(--lucent-line-height-base)",relaxed:"var(--lucent-line-height-relaxed)"},Qe={base:"var(--lucent-font-family-base)",mono:"var(--lucent-font-family-mono)",display:"var(--lucent-font-family-display)"};function z({as:n="p",size:t="md",weight:a="regular",color:r="primary",align:o="left",lineHeight:i="base",family:s="base",truncate:l=!1,children:d,style:p,...f}){const m={fontSize:Ye[t],fontWeight:Xe[a],color:Ke[r],textAlign:o,lineHeight:Je[i],fontFamily:Qe[s],margin:0,...l&&{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},...p},c=n;return e.jsx(c,{style:m,...f,children:d})}const Ze={id:"text",name:"Text",tier:"atom",domain:"neutral",specVersion:"0.1",description:"Polymorphic typography primitive that maps semantic HTML elements to design-system type scales.",designIntent:"Text is the single source of truth for typography in Lucent UI. Rather than hard-coding font sizes and colors directly in components, every piece of rendered text should pass through this atom. The `as` prop controls the HTML element (and therefore semantic meaning), while `size`, `weight`, `color`, and `lineHeight` control appearance independently — decoupling semantics from style. Use `truncate` only in constrained-width containers where overflow must be prevented; it applies overflow: hidden + text-overflow: ellipsis. The `family` prop enables inline code or monospaced content without switching components.",props:[{name:"as",type:"enum",required:!1,default:"p",description:"HTML element to render. Controls semantic meaning independently of visual style.",enumValues:["p","h1","h2","h3","h4","h5","h6","span","div","label","strong","em","code"]},{name:"size",type:"enum",required:!1,default:"md",description:"Font size from the type scale. Maps to the corresponding --lucent-font-size-* token.",enumValues:["xs","sm","md","lg","xl","2xl","3xl"]},{name:"weight",type:"enum",required:!1,default:"regular",description:"Font weight. Maps to --lucent-font-weight-* tokens.",enumValues:["regular","medium","semibold","bold"]},{name:"color",type:"enum",required:!1,default:"primary",description:"Semantic text color. All values adapt automatically in light and dark themes.",enumValues:["primary","secondary","disabled","inverse","onAccent","success","warning","danger","info"]},{name:"align",type:"enum",required:!1,default:"left",description:"Text alignment.",enumValues:["left","center","right"]},{name:"lineHeight",type:"enum",required:!1,default:"base",description:"Line height. tight=1.25, base=1.5, relaxed=1.75.",enumValues:["tight","base","relaxed"]},{name:"family",type:"enum",required:!1,default:"base",description:"Font family. Use mono for code or tabular data. Use display (Unbounded) for headings and marketing copy.",enumValues:["base","mono","display"]},{name:"truncate",type:"boolean",required:!1,default:"false",description:"Clips overflow text with an ellipsis. Requires the container to have a constrained width."},{name:"children",type:"ReactNode",required:!0,description:"The text content to render."},{name:"style",type:"object",required:!1,description:"Inline style overrides. Applied after computed token styles."}],usageExamples:[{title:"Heading",code:'<Text as="h1" size="3xl" weight="bold">Page title</Text>'},{title:"Body paragraph",code:'<Text size="md" color="secondary">Supporting copy goes here.</Text>'},{title:"Label",code:'<Text as="label" size="sm" weight="medium" htmlFor="email">Email</Text>'},{title:"Inline code",code:'<Text as="code" family="mono" size="sm">const x = 1;</Text>'},{title:"Truncated",code:"<Text truncate style={{ maxWidth: 200 }}>Very long text that will be clipped</Text>"},{title:"Status color",code:'<Text color="danger" size="xs">This field is required</Text>'}],compositionGraph:[],accessibility:{notes:'The rendered element determines the implicit ARIA role. Use heading elements (h1–h6) for document headings so screen readers can navigate the page structure. Use `as="label"` with `htmlFor` to associate labels with form controls. Decorative text needs no additional ARIA.'}};function et({children:n,href:t,isActive:a=!1,icon:r,disabled:o=!1,onClick:i,as:s,style:l}){const d=s??"a";return e.jsxs(d,{href:o?void 0:t,onClick:o?void 0:i,"aria-current":a?"page":void 0,"aria-disabled":o||void 0,style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-2)",padding:"var(--lucent-space-2) var(--lucent-space-3) var(--lucent-space-2) var(--lucent-space-4)",borderRadius:"var(--lucent-radius-md)",background:o?"transparent":a?"var(--lucent-accent-default)":"transparent",color:o?"var(--lucent-text-disabled)":a?"var(--lucent-text-on-accent)":"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-md)",fontWeight:a?"var(--lucent-font-weight-medium)":"var(--lucent-font-weight-regular)",textDecoration:"none",cursor:o?"not-allowed":"pointer",transition:"background var(--lucent-duration-fast) var(--lucent-easing-default), color var(--lucent-duration-fast) var(--lucent-easing-default)",userSelect:"none",boxSizing:"border-box",...l},onMouseEnter:p=>{!o&&!a&&(p.currentTarget.style.background="var(--lucent-bg-muted)")},onMouseLeave:p=>{!o&&!a&&(p.currentTarget.style.background="transparent")},children:[r!=null&&e.jsx("span",{style:{display:"flex",flexShrink:0,color:"inherit"},children:r}),n]})}function tt({label:n,htmlFor:t,required:a=!1,helperText:r,errorMessage:o,children:i,style:s}){const l=o??r,d=o?"danger":"secondary";return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-2)",...s},children:[n&&e.jsxs("div",{style:{display:"flex",alignItems:"baseline",gap:"var(--lucent-space-1)"},children:[e.jsx(z,{as:"label",size:"sm",weight:"medium",lineHeight:"tight",...t!==void 0&&{htmlFor:t},children:n}),a&&e.jsx(z,{as:"span",size:"sm",color:"danger",lineHeight:"tight","aria-hidden":"true",children:"*"})]}),i,l&&e.jsx(z,{size:"xs",color:d,lineHeight:"tight",children:l})]})}const nt={id:"form-field",name:"FormField",tier:"molecule",domain:"neutral",specVersion:"0.1",description:"Wraps any form control (Input, Select, Textarea) with a label, helper text, and validation message.",designIntent:"FormField standardises the vertical rhythm around form controls. A label is linked to the control via htmlFor so screen readers announce it correctly. The required asterisk is decorative (aria-hidden) because the actual required state should be communicated on the input via aria-required. Helper text provides proactive guidance; errorMessage replaces it when validation fails, using danger color to draw attention. The gap between elements uses space-2 to create a tight but breathable stack.",props:[{name:"label",type:"string",required:!1,description:"Label text rendered above the control as a <label> element."},{name:"htmlFor",type:"string",required:!1,description:"ID of the form control this label describes. Forwarded to the label htmlFor attribute."},{name:"required",type:"boolean",required:!1,default:"false",description:"Appends a danger-colored asterisk after the label text."},{name:"helperText",type:"string",required:!1,description:"Secondary text below the control providing guidance. Hidden when errorMessage is set."},{name:"errorMessage",type:"string",required:!1,description:"Validation error shown in danger color below the control. Replaces helperText when set."},{name:"children",type:"ReactNode",required:!0,description:"The form control to wrap — typically Input, Select, or Textarea."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Basic field",code:`<FormField label="Email" htmlFor="email">
|
|
56
56
|
<Input id="email" placeholder="you@example.com" />
|
|
57
57
|
</FormField>`},{title:"Required with helper",code:`<FormField label="Username" htmlFor="username" required helperText="Letters and numbers only">
|
|
58
58
|
<Input id="username" />
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<Input id="pw" type="password" />
|
|
61
61
|
</FormField>`},{title:"Wrapping a Select",code:`<FormField label="Country" htmlFor="country">
|
|
62
62
|
<Select id="country" options={countryOptions} />
|
|
63
|
-
</FormField>`}],compositionGraph:[{componentId:"text",componentName:"Text",role:"Label, helper text, and error message",required:!1}],accessibility:{ariaAttributes:["aria-required","aria-describedby"],notes:'Link the wrapped control to an error message using aria-describedby on the control and a matching id on the error element for full screen reader support. The required asterisk is aria-hidden; set aria-required="true" on the control itself.'}},
|
|
63
|
+
</FormField>`}],compositionGraph:[{componentId:"text",componentName:"Text",role:"Label, helper text, and error message",required:!1}],accessibility:{ariaAttributes:["aria-required","aria-describedby"],notes:'Link the wrapped control to an error message using aria-describedby on the control and a matching id on the error element for full screen reader support. The required asterisk is aria-hidden; set aria-required="true" on the control itself.'}},at=()=>e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":"true",children:[e.jsx("circle",{cx:"6.5",cy:"6.5",r:"4",stroke:"currentColor",strokeWidth:"1.5"}),e.jsx("path",{d:"M9.5 9.5L13 13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})]}),rt=()=>e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none","aria-hidden":"true",children:e.jsx("path",{d:"M3 3L11 11M11 3L3 11",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})});function ot({value:n,onChange:t,placeholder:a="Search…",results:r=[],onResultSelect:o,isLoading:i=!1,disabled:s=!1,id:l,style:d}){const[p,f]=g.useState(!1),[m,c]=g.useState(null),u=g.useRef(null),b=p&&r.length>0,h=()=>{t("")},x=y=>{o==null||o(y),f(!1)},w=()=>{u.current=setTimeout(()=>f(!1),150)},v=()=>{u.current&&clearTimeout(u.current),f(!0)},S=i?e.jsx(L,{size:"sm"}):n?e.jsx("button",{type:"button","aria-label":"Clear search",onClick:h,style:{display:"flex",alignItems:"center",background:"none",border:"none",cursor:"pointer",padding:2,borderRadius:"var(--lucent-radius-sm)",color:"var(--lucent-text-secondary)"},onMouseEnter:y=>{y.currentTarget.style.color="var(--lucent-text-primary)"},onMouseLeave:y=>{y.currentTarget.style.color="var(--lucent-text-secondary)"},children:e.jsx(rt,{})}):null;return e.jsxs("div",{style:{position:"relative",...d},children:[e.jsx(E,{id:l,type:"search",value:n,onChange:y=>t(y.target.value),placeholder:a,disabled:s,leftElement:e.jsx(at,{}),rightElement:S??void 0,onFocus:v,onBlur:w}),b&&e.jsx("div",{role:"listbox",style:{position:"absolute",top:"calc(100% + var(--lucent-space-1))",left:0,right:0,zIndex:50,background:"var(--lucent-surface-overlay)",border:"1px solid var(--lucent-border-default)",borderRadius:"var(--lucent-radius-md)",boxShadow:"var(--lucent-shadow-md)",overflow:"hidden"},children:r.map((y,I)=>e.jsx("div",{role:"option","aria-selected":!1,onMouseDown:()=>x(y),onMouseEnter:()=>c(I),onMouseLeave:()=>c(null),style:{padding:"var(--lucent-space-2) var(--lucent-space-3)",cursor:"pointer",background:m===I?"var(--lucent-bg-subtle)":"transparent",transition:"background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:e.jsx(z,{as:"span",size:"md",children:y.label})},y.id))})]})}const it={id:"search-input",name:"SearchInput",tier:"molecule",domain:"neutral",specVersion:"0.1",description:"A search field with a built-in magnifier icon, clear button, and an optional results dropdown.",designIntent:"SearchInput is intentionally dumb about filtering — the consumer passes already-filtered results so the component stays stateless and flexible. The clear button appears only when the input has a value, keeping the right side clean at rest. The results dropdown is rendered absolutely below the input and closes after a 150ms delay on blur to allow result clicks to register before focus is lost. Spinner replaces the clear button during loading to communicate async state without layout shift.",props:[{name:"value",type:"string",required:!0,description:"Controlled input value."},{name:"onChange",type:"function",required:!0,description:"Called with the new string value whenever the input changes."},{name:"placeholder",type:"string",required:!1,default:'"Search…"',description:"Placeholder text for the input."},{name:"results",type:"array",required:!1,default:"[]",description:"Pre-filtered list of SearchResult objects ({ id, label }) to display in the dropdown."},{name:"onResultSelect",type:"function",required:!1,description:"Called with the selected SearchResult when a dropdown item is clicked."},{name:"isLoading",type:"boolean",required:!1,default:"false",description:"Shows a spinner in the right slot to indicate async search in progress."},{name:"disabled",type:"boolean",required:!1,default:"false",description:"Disables the input."},{name:"id",type:"string",required:!1,description:"HTML id forwarded to the underlying input element."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"Basic controlled search",code:`const [query, setQuery] = useState('');
|
|
64
64
|
const [results, setResults] = useState([]);
|
|
65
65
|
|
|
66
66
|
<SearchInput
|
|
@@ -68,7 +68,7 @@ const [results, setResults] = useState([]);
|
|
|
68
68
|
onChange={(v) => { setQuery(v); setResults(filter(v)); }}
|
|
69
69
|
results={results}
|
|
70
70
|
onResultSelect={(r) => console.log(r)}
|
|
71
|
-
/>`},{title:"Loading state",code:"<SearchInput value={query} onChange={setQuery} isLoading={isFetching} results={[]} />"}],compositionGraph:[{componentId:"input",componentName:"Input",role:"Search text field with icon slots",required:!0},{componentId:"spinner",componentName:"Spinner",role:"Loading indicator in the right slot",required:!1}],accessibility:{role:"combobox",ariaAttributes:["aria-expanded","aria-haspopup","aria-label"],keyboardInteractions:["Enter to select focused result","Escape to close dropdown"],notes:'The results list uses role="listbox" with role="option" items. For full keyboard navigation (arrow keys to move between results), wire up onKeyDown on the Input and manage an activeIndex state.'}},
|
|
71
|
+
/>`},{title:"Loading state",code:"<SearchInput value={query} onChange={setQuery} isLoading={isFetching} results={[]} />"}],compositionGraph:[{componentId:"input",componentName:"Input",role:"Search text field with icon slots",required:!0},{componentId:"spinner",componentName:"Spinner",role:"Loading indicator in the right slot",required:!1}],accessibility:{role:"combobox",ariaAttributes:["aria-expanded","aria-haspopup","aria-label"],keyboardInteractions:["Enter to select focused result","Escape to close dropdown"],notes:'The results list uses role="listbox" with role="option" items. For full keyboard navigation (arrow keys to move between results), wire up onKeyDown on the Input and manage an activeIndex state.'}},st={none:"0",sm:"var(--lucent-space-3)",md:"var(--lucent-space-4)",lg:"var(--lucent-space-6)"},lt={none:"var(--lucent-shadow-none)",sm:"var(--lucent-shadow-sm)",md:"var(--lucent-shadow-md)",lg:"var(--lucent-shadow-lg)"},ct={none:"var(--lucent-radius-none)",sm:"var(--lucent-radius-sm)",md:"var(--lucent-radius-md)",lg:"var(--lucent-radius-lg)"};function dt({header:n,footer:t,children:a,padding:r="md",shadow:o="sm",radius:i="md",style:s}){const l=st[r],d=ct[i];return e.jsxs("div",{style:{display:"flex",flexDirection:"column",background:"var(--lucent-surface-default)",border:"1px solid var(--lucent-border-default)",borderRadius:d,boxShadow:lt[o],overflow:"hidden",boxSizing:"border-box",...s},children:[n!=null&&e.jsx("div",{style:{padding:l,borderBottom:"1px solid var(--lucent-border-default)"},children:n}),e.jsx("div",{style:{padding:l,flex:1},children:a}),t!=null&&e.jsx("div",{style:{padding:l,borderTop:"1px solid var(--lucent-border-default)"},children:t})]})}const ut={id:"card",name:"Card",tier:"molecule",domain:"neutral",specVersion:"0.1",description:"A surface container with optional header, body, and footer slots, configurable padding, shadow, and radius.",designIntent:"Card provides a consistent elevated surface for grouping related content. The header and footer slots are separated from the body by a border-default divider, giving visual structure without requiring the consumer to manage spacing. Padding, shadow, and radius are all configurable to accommodate flat/ghost cards, modal-like surfaces, and compact data-dense layouts. The overflow: hidden ensures children respect the border-radius without needing additional clipping.",props:[{name:"children",type:"ReactNode",required:!0,description:"The card body content."},{name:"header",type:"ReactNode",required:!1,description:"Content rendered in the header slot, separated from the body by a divider."},{name:"footer",type:"ReactNode",required:!1,description:"Content rendered in the footer slot, separated from the body by a divider."},{name:"padding",type:"enum",required:!1,default:"md",description:"Inner padding applied equally to header, body, and footer.",enumValues:["none","sm","md","lg"]},{name:"shadow",type:"enum",required:!1,default:"sm",description:"Box shadow elevation.",enumValues:["none","sm","md","lg"]},{name:"radius",type:"enum",required:!1,default:"md",description:"Border radius of the card.",enumValues:["none","sm","md","lg"]},{name:"style",type:"object",required:!1,description:"Inline style overrides for the card wrapper."}],usageExamples:[{title:"Simple card",code:`<Card>
|
|
72
72
|
<Text>Some content here.</Text>
|
|
73
73
|
</Card>`},{title:"With header and footer",code:`<Card
|
|
74
74
|
header={<Text weight="semibold">Card title</Text>}
|
|
@@ -77,11 +77,11 @@ const [results, setResults] = useState([]);
|
|
|
77
77
|
<Text color="secondary">Card body content goes here.</Text>
|
|
78
78
|
</Card>`},{title:"Flat variant",code:`<Card shadow="none" radius="sm" padding="sm">
|
|
79
79
|
<Text size="sm">Compact flat card</Text>
|
|
80
|
-
</Card>`}],compositionGraph:[],accessibility:{notes:"Card has no implicit ARIA role. If the card represents a landmark, wrap it in a <section> or <article> and provide an aria-label. For interactive cards (clickable), make the wrapper a <button> or <a> and ensure focus styles are visible."}},
|
|
80
|
+
</Card>`}],compositionGraph:[],accessibility:{notes:"Card has no implicit ARIA role. If the card represents a landmark, wrap it in a <section> or <article> and provide an aria-label. For interactive cards (clickable), make the wrapper a <button> or <a> and ensure focus styles are visible."}},pt={info:{bg:"var(--lucent-info-subtle)",border:"var(--lucent-info-default)",iconColor:"var(--lucent-info-text)",textColor:"info"},success:{bg:"var(--lucent-success-subtle)",border:"var(--lucent-success-default)",iconColor:"var(--lucent-success-text)",textColor:"success"},warning:{bg:"var(--lucent-warning-subtle)",border:"var(--lucent-warning-default)",iconColor:"var(--lucent-warning-text)",textColor:"warning"},danger:{bg:"var(--lucent-danger-subtle)",border:"var(--lucent-danger-default)",iconColor:"var(--lucent-danger-text)",textColor:"danger"}},ft=()=>e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":"true",children:[e.jsx("circle",{cx:"8",cy:"8",r:"6.5",stroke:"currentColor",strokeWidth:"1.5"}),e.jsx("path",{d:"M8 5.5V8.5M8 10.5V11",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})]}),mt=()=>e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":"true",children:[e.jsx("circle",{cx:"8",cy:"8",r:"6.5",stroke:"currentColor",strokeWidth:"1.5"}),e.jsx("path",{d:"M5 8L7 10L11 6",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})]}),ht=()=>e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":"true",children:[e.jsx("path",{d:"M8 2L14.5 13H1.5L8 2Z",stroke:"currentColor",strokeWidth:"1.5",strokeLinejoin:"round"}),e.jsx("path",{d:"M8 6V9M8 11V11.5",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})]}),gt=()=>e.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":"true",children:[e.jsx("circle",{cx:"8",cy:"8",r:"6.5",stroke:"currentColor",strokeWidth:"1.5"}),e.jsx("path",{d:"M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})]}),bt={info:e.jsx(ft,{}),success:e.jsx(mt,{}),warning:e.jsx(ht,{}),danger:e.jsx(gt,{})},vt=()=>e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none","aria-hidden":"true",children:e.jsx("path",{d:"M3 3L11 11M11 3L3 11",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})});function yt({variant:n="info",title:t,children:a,onDismiss:r,icon:o,style:i}){const s=pt[n],l=o??bt[n];return e.jsxs("div",{role:"alert",style:{display:"flex",alignItems:"flex-start",gap:"var(--lucent-space-3)",padding:"var(--lucent-space-3) var(--lucent-space-4)",background:s.bg,border:`1px solid ${s.border}`,borderRadius:"var(--lucent-radius-md)",boxSizing:"border-box",...i},children:[e.jsx("span",{style:{flexShrink:0,color:s.iconColor,display:"flex",alignItems:"center",paddingTop:2},children:l}),e.jsxs("div",{style:{flex:1,display:"flex",flexDirection:"column",gap:"var(--lucent-space-1)"},children:[t&&e.jsx(z,{as:"span",size:"sm",weight:"semibold",color:s.textColor,lineHeight:"tight",children:t}),a&&e.jsx(z,{as:"span",size:"sm",color:s.textColor,lineHeight:"base",children:a})]}),r&&e.jsx("button",{type:"button","aria-label":"Dismiss",onClick:r,style:{flexShrink:0,display:"flex",alignItems:"center",background:"none",border:"none",cursor:"pointer",padding:2,borderRadius:"var(--lucent-radius-sm)",color:s.iconColor,opacity:.7},onMouseEnter:d=>{d.currentTarget.style.opacity="1"},onMouseLeave:d=>{d.currentTarget.style.opacity="0.7"},children:e.jsx(vt,{})})]})}const xt={id:"alert",name:"Alert",tier:"molecule",domain:"neutral",specVersion:"0.1",description:"An inline feedback banner with info, success, warning, and danger variants, optional title, and dismiss button.",designIntent:'Alert uses role="alert" so screen readers announce the message immediately when it appears. Each variant has a built-in icon that communicates intent visually; the icon can be overridden for custom scenarios. Title and body are both optional — you can show either, both, or just an icon with a body. The dismiss button is only rendered when onDismiss is provided, keeping the layout clean for non-dismissible alerts. All colors use status semantic tokens so they adapt correctly between light and dark themes.',props:[{name:"variant",type:"enum",required:!1,default:"info",description:"Visual and semantic variant of the alert.",enumValues:["info","success","warning","danger"]},{name:"title",type:"string",required:!1,description:"Bold title line rendered above the body."},{name:"children",type:"ReactNode",required:!1,description:"Alert body content — typically a short sentence or ReactNode."},{name:"onDismiss",type:"function",required:!1,description:"When provided, renders a dismiss (×) button and calls this handler on click."},{name:"icon",type:"ReactNode",required:!1,description:"Custom icon to replace the built-in variant icon."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the alert wrapper."}],usageExamples:[{title:"Info with body",code:'<Alert variant="info">Your changes have been saved as a draft.</Alert>'},{title:"With title and dismiss",code:`<Alert variant="danger" title="Payment failed" onDismiss={() => setVisible(false)}>
|
|
81
81
|
Check your card details and try again.
|
|
82
82
|
</Alert>`},{title:"Success confirmation",code:`<Alert variant="success" title="Order placed!">
|
|
83
83
|
You'll receive a confirmation email shortly.
|
|
84
|
-
</Alert>`}],compositionGraph:[{componentId:"text",componentName:"Text",role:"Title and body content",required:!1}],accessibility:{role:"alert",ariaAttributes:["aria-label"],notes:'role="alert" causes screen readers to announce the content immediately when rendered. For non-urgent status messages, consider using role="status" instead by overriding via style/wrapper.'}};function
|
|
84
|
+
</Alert>`}],compositionGraph:[{componentId:"text",componentName:"Text",role:"Title and body content",required:!1}],accessibility:{role:"alert",ariaAttributes:["aria-label"],notes:'role="alert" causes screen readers to announce the content immediately when rendered. For non-urgent status messages, consider using role="status" instead by overriding via style/wrapper.'}};function wt({illustration:n,title:t,description:a,action:r,style:o}){return e.jsxs("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",gap:"var(--lucent-space-4)",padding:"var(--lucent-space-8)",textAlign:"center",...o},children:[n!=null&&e.jsx("div",{style:{width:64,height:64,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--lucent-text-secondary)"},children:n}),e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-2)"},children:[e.jsx(z,{as:"h3",size:"lg",weight:"semibold",align:"center",lineHeight:"tight",children:t}),a&&e.jsx(z,{size:"sm",color:"secondary",align:"center",lineHeight:"relaxed",children:a})]}),r!=null&&e.jsx("div",{children:r})]})}const St={id:"empty-state",name:"EmptyState",tier:"molecule",domain:"neutral",specVersion:"0.1",description:"A centered placeholder shown when a list or page has no content, with an optional illustration, title, description, and CTA.",designIntent:"EmptyState communicates the absence of data in a constructive way. The illustration slot accepts any ReactNode — an Icon atom, a custom SVG, or an image — and constrains it to a 64px square to maintain visual consistency. Title is required to ensure the state is always named; description is optional for additional context. The action slot accepts any ReactNode (typically a Button) so the consumer controls variant and label without prescribing them. The entire layout is center-aligned and padded to sit naturally inside a Card or page section.",props:[{name:"title",type:"string",required:!0,description:'Short headline naming the empty state (e.g. "No results found").'},{name:"illustration",type:"ReactNode",required:!1,description:"Icon, SVG, or image rendered above the title. Constrained to a 64px container."},{name:"description",type:"string",required:!1,description:"Secondary text below the title providing context or next steps."},{name:"action",type:"ReactNode",required:!1,description:"Call-to-action rendered below the description — typically a Button."},{name:"style",type:"object",required:!1,description:"Inline style overrides for the outer wrapper."}],usageExamples:[{title:"No search results",code:`<EmptyState
|
|
85
85
|
illustration={<Icon size="xl"><SearchIcon /></Icon>}
|
|
86
86
|
title="No results found"
|
|
87
87
|
description="Try adjusting your search or filter to find what you're looking for."
|
|
@@ -92,21 +92,26 @@ const [results, setResults] = useState([]);
|
|
|
92
92
|
action={<Button variant="primary">New project</Button>}
|
|
93
93
|
/>`},{title:"Inside a Card",code:`<Card>
|
|
94
94
|
<EmptyState title="Nothing here" description="Add items to see them listed." />
|
|
95
|
-
</Card>`}],compositionGraph:[{componentId:"text",componentName:"Text",role:"Title and description",required:!0}],accessibility:{notes:"The title renders as an h3 by default. If EmptyState appears inside a section with its own heading hierarchy, override by passing a Text component as part of a custom layout. Ensure the action element has a descriptive label for screen readers."}},
|
|
95
|
+
</Card>`}],compositionGraph:[{componentId:"text",componentName:"Text",role:"Title and description",required:!0}],accessibility:{notes:"The title renders as an h3 by default. If EmptyState appears inside a section with its own heading hierarchy, override by passing a Text component as part of a custom layout. Ensure the action element has a descriptive label for screen readers."}},kt={text:"1em",circle:40,rectangle:40},Tt={text:"var(--lucent-radius-sm)",circle:"var(--lucent-radius-full)",rectangle:"var(--lucent-radius-md)"};function A({width:n,height:t,radius:a,animate:r,style:o}){return e.jsx("span",{style:{display:"block",width:typeof n=="number"?`${n}px`:n,height:typeof t=="number"?`${t}px`:t,borderRadius:a,background:r?"linear-gradient(90deg, var(--lucent-bg-muted) 25%, var(--lucent-bg-subtle) 50%, var(--lucent-bg-muted) 75%)":"var(--lucent-bg-muted)",backgroundSize:r?"200% 100%":void 0,animation:r?"lucent-skeleton-shimmer 1.6s ease-in-out infinite":void 0,flexShrink:0,...o}})}function Ct({variant:n="rectangle",width:t="100%",height:a,lines:r=1,animate:o=!0,radius:i,style:s}){const l=a??kt[n],d=i??Tt[n],p=o?e.jsx("style",{children:`
|
|
96
96
|
@keyframes lucent-skeleton-shimmer {
|
|
97
97
|
0% { background-position: 200% 0; }
|
|
98
98
|
100% { background-position: -200% 0; }
|
|
99
99
|
}
|
|
100
|
-
`}):null;return n==="text"&&r>1?e.jsxs(e.Fragment,{children:[
|
|
100
|
+
`}):null;return n==="text"&&r>1?e.jsxs(e.Fragment,{children:[p,e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"var(--lucent-space-2)",...s},children:Array.from({length:r}).map((f,m)=>e.jsx(A,{width:m===r-1?"70%":t,height:l,radius:d,animate:o},m))})]}):e.jsxs(e.Fragment,{children:[p,e.jsx(A,{width:n==="circle"?a??40:t,height:l,radius:d,animate:o,...s!==void 0&&{style:s}})]})}const It={id:"skeleton",name:"Skeleton",tier:"molecule",domain:"neutral",specVersion:"0.1",description:"Animated placeholder that matches the shape of content while it loads.",designIntent:"Skeleton uses a shimmer animation to communicate that content is loading without showing a spinner. The three variants (text, circle, rectangle) cover the most common content shapes: inline text, avatars/thumbnails, and generic content blocks. The text variant with lines > 1 mimics a paragraph by stacking multiple text skeletons and shortening the last line to 70%, which is a widely recognised convention for body copy placeholders. The shimmer gradient uses bg-muted and bg-subtle so it adapts correctly in both light and dark themes without hard-coded colors.",props:[{name:"variant",type:"enum",required:!1,default:"rectangle",description:"Shape of the skeleton — text (1em tall), circle (equal width/height), or rectangle.",enumValues:["text","circle","rectangle"]},{name:"width",type:"string",required:!1,default:'"100%"',description:"Width of the skeleton. Accepts any CSS value or a number (interpreted as px)."},{name:"height",type:"string",required:!1,description:"Height of the skeleton. Defaults: text=1em, circle=40px, rectangle=40px."},{name:"lines",type:"number",required:!1,default:"1",description:"Number of stacked text lines to render. Only applies to the text variant."},{name:"animate",type:"boolean",required:!1,default:"true",description:"Enables the shimmer animation. Set to false to render a static placeholder."},{name:"radius",type:"string",required:!1,description:"Override the default border-radius for the variant."},{name:"style",type:"object",required:!1,description:"Inline style overrides."}],usageExamples:[{title:"Paragraph placeholder",code:'<Skeleton variant="text" lines={3} />'},{title:"Avatar placeholder",code:'<Skeleton variant="circle" width={40} height={40} />'},{title:"Card placeholder",code:`<Card>
|
|
101
101
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
102
102
|
<Skeleton variant="rectangle" height={160} />
|
|
103
103
|
<Skeleton variant="text" lines={2} />
|
|
104
104
|
<Skeleton variant="text" width="40%" />
|
|
105
105
|
</div>
|
|
106
|
-
</Card>`},{title:"Static (no animation)",code:'<Skeleton variant="rectangle" width={200} height={32} animate={false} />'}],compositionGraph:[],accessibility:{ariaAttributes:["aria-busy","aria-label"],notes:'Wrap loading regions with aria-busy="true" on the container so screen readers know content is loading. Individual Skeleton elements are presentational and do not need ARIA attributes themselves.'}}
|
|
106
|
+
</Card>`},{title:"Static (no animation)",code:'<Skeleton variant="rectangle" width={200} height={32} animate={false} />'}],compositionGraph:[],accessibility:{ariaAttributes:["aria-busy","aria-label"],notes:'Wrap loading regions with aria-busy="true" on the container so screen readers know content is loading. Individual Skeleton elements are presentational and do not need ARIA attributes themselves.'}};function jt({items:n,separator:t="/",style:a}){return e.jsx("nav",{"aria-label":"Breadcrumb",style:a,children:e.jsx("ol",{style:{display:"flex",alignItems:"center",flexWrap:"wrap",gap:"var(--lucent-space-1)",listStyle:"none",margin:0,padding:0,fontFamily:"var(--lucent-font-family-base)"},children:n.map((r,o)=>{const i=o===n.length-1;return e.jsxs("li",{style:{display:"flex",alignItems:"center",gap:"var(--lucent-space-1)"},children:[i?e.jsx(z,{size:"sm",color:"primary",as:"span","aria-current":"page",children:r.label}):r.href!=null?e.jsx("a",{href:r.href,onClick:r.onClick,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",textDecoration:"none",fontFamily:"var(--lucent-font-family-base)",transition:"color var(--lucent-duration-fast) var(--lucent-easing-default)"},onMouseEnter:s=>{s.currentTarget.style.color="var(--lucent-text-primary)"},onMouseLeave:s=>{s.currentTarget.style.color="var(--lucent-text-secondary)"},children:r.label}):e.jsx("button",{onClick:r.onClick,style:{fontSize:"var(--lucent-font-size-sm)",color:"var(--lucent-text-secondary)",fontFamily:"var(--lucent-font-family-base)",background:"none",border:"none",padding:0,cursor:"pointer",transition:"color var(--lucent-duration-fast) var(--lucent-easing-default)"},onMouseEnter:s=>{s.currentTarget.style.color="var(--lucent-text-primary)"},onMouseLeave:s=>{s.currentTarget.style.color="var(--lucent-text-secondary)"},children:r.label}),!i&&e.jsx("span",{"aria-hidden":!0,style:{color:"var(--lucent-text-disabled)",fontSize:"var(--lucent-font-size-sm)",userSelect:"none"},children:t})]},o)})})})}function zt({tabs:n,defaultValue:t,value:a,onChange:r,style:o}){var w;const i=a!==void 0,[s,l]=g.useState(t??((w=n[0])==null?void 0:w.value)??""),d=i?a:s,[p,f]=g.useState(null),m=g.useRef([]),[c,u]=g.useState(null),b=g.useRef(!1);g.useLayoutEffect(()=>{const v=n.findIndex(y=>y.value===d),S=m.current[v];S&&(u({left:S.offsetLeft,width:S.offsetWidth,animate:b.current}),b.current=!0)},[d,n]);const h=v=>{i||l(v),r==null||r(v)},x=(v,S)=>{var M;const y=n.map((q,j)=>q.disabled?-1:j).filter(q=>q!==-1),I=y.indexOf(S);let T=-1;v.key==="ArrowRight"&&(T=y[(I+1)%y.length]??-1),v.key==="ArrowLeft"&&(T=y[(I-1+y.length)%y.length]??-1),v.key==="Home"&&(T=y[0]??-1),v.key==="End"&&(T=y[y.length-1]??-1),T!==-1&&(v.preventDefault(),(M=m.current[T])==null||M.focus(),h(n[T].value))};return e.jsxs("div",{style:{display:"flex",flexDirection:"column",...o},children:[e.jsxs("div",{role:"tablist",style:{position:"relative",display:"flex",borderBottom:"1px solid var(--lucent-border-default)"},children:[n.map((v,S)=>{const y=v.value===d,I=v.disabled??!1;return e.jsx("button",{ref:T=>{m.current[S]=T},role:"tab","aria-selected":y,"aria-controls":`lucent-tabpanel-${v.value}`,id:`lucent-tab-${v.value}`,disabled:I,tabIndex:y?0:-1,onClick:()=>{I||h(v.value)},onKeyDown:T=>x(T,S),onMouseEnter:()=>{I||f(S)},onMouseLeave:()=>f(null),style:{padding:"var(--lucent-space-1) var(--lucent-space-2) var(--lucent-space-3)",background:"none",border:"none",cursor:I?"not-allowed":"pointer",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-md)",fontWeight:y?"var(--lucent-font-weight-medium)":"var(--lucent-font-weight-regular)",color:I?"var(--lucent-text-disabled)":y?"var(--lucent-text-primary)":"var(--lucent-text-secondary)",transition:"color var(--lucent-duration-fast) var(--lucent-easing-default)",whiteSpace:"nowrap",outline:"none"},children:e.jsx("span",{style:{display:"block",padding:"var(--lucent-space-1) var(--lucent-space-3)",borderRadius:"var(--lucent-radius-md)",background:p===S&&!y?"var(--lucent-bg-subtle)":"transparent",transition:"background var(--lucent-duration-fast) var(--lucent-easing-default)"},children:v.label})},v.value)}),c!=null&&e.jsx("span",{"aria-hidden":!0,style:{position:"absolute",bottom:0,left:c.left,width:c.width,height:2,background:"var(--lucent-accent-default)",borderRadius:"var(--lucent-radius-sm)",transition:c.animate?"left 220ms var(--lucent-easing-default), width 220ms var(--lucent-easing-default)":"none"}})]}),n.map(v=>e.jsx("div",{role:"tabpanel",id:`lucent-tabpanel-${v.value}`,"aria-labelledby":`lucent-tab-${v.value}`,hidden:v.value!==d,style:{padding:"var(--lucent-space-4) 0",outline:"none"},tabIndex:0,children:v.content},v.value))]})}const Mt=`
|
|
107
|
+
@keyframes lucent-collapsible-open {
|
|
108
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
109
|
+
to { opacity: 1; transform: translateY(0); }
|
|
110
|
+
}
|
|
111
|
+
`;function qt({trigger:n,children:t,defaultOpen:a=!1,open:r,onOpenChange:o,style:i}){const s=r!==void 0,[l,d]=g.useState(a),p=s?r:l,f=g.useRef(null),[m,c]=g.useState(p?void 0:0),u=g.useRef(!1);g.useEffect(()=>{const h=f.current;if(h)if(p){const x=h.scrollHeight;c(x),u.current=!0;const w=setTimeout(()=>{c(void 0),u.current=!1},220);return()=>clearTimeout(w)}else c(h.scrollHeight),h.getBoundingClientRect(),c(0)},[p]);const b=()=>{const h=!p;s||d(h),o==null||o(h)};return e.jsxs(e.Fragment,{children:[e.jsx("style",{children:Mt}),e.jsxs("div",{style:{display:"flex",flexDirection:"column",fontFamily:"var(--lucent-font-family-base)",fontSize:"var(--lucent-font-size-md)",...i},children:[e.jsxs("button",{onClick:b,"aria-expanded":p,style:{display:"flex",alignItems:"center",justifyContent:"space-between",width:"100%",background:"none",border:"none",padding:"var(--lucent-space-3) var(--lucent-space-4)",cursor:"pointer",textAlign:"left",outline:"none",fontFamily:"inherit",fontSize:"inherit"},children:[e.jsx("span",{style:{flex:1},children:n}),e.jsx(Et,{open:p})]}),e.jsx("div",{ref:f,"aria-hidden":!p,style:{overflow:"hidden",height:m!==void 0?m:"auto",transition:"height 200ms var(--lucent-easing-default)"},children:e.jsx("div",{style:{padding:"var(--lucent-space-2) var(--lucent-space-4) var(--lucent-space-3)",animation:p?"lucent-collapsible-open 200ms var(--lucent-easing-default) forwards":void 0},children:t})})]})]})}function Et({open:n}){return e.jsx("svg",{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:n?"rotate(180deg)":"rotate(0deg)",transition:"transform 200ms var(--lucent-easing-default)"},children:e.jsx("polyline",{points:"6 9 12 15 18 9"})})}function N(n){return typeof n=="number"?`${n}px`:n}function Rt({children:n,header:t,sidebar:a,sidebarWidth:r=240,headerHeight:o=48,sidebarCollapsed:i=!1,mainStyle:s,style:l}){const d=N(o),p=N(r);return e.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100vh",overflow:"hidden",fontFamily:"var(--lucent-font-family-base)",...l},children:[t!=null&&e.jsx("div",{style:{flexShrink:0,height:d,zIndex:10,background:"var(--lucent-surface-default)"},children:t}),e.jsxs("div",{style:{display:"flex",flex:1,overflow:"hidden"},children:[a!=null&&e.jsx("div",{style:{width:i?0:p,flexShrink:0,overflow:"hidden",overflowY:i?"hidden":"auto",background:"var(--lucent-surface-default)",transition:"width 200ms var(--lucent-easing-default)"},children:a}),e.jsx("main",{style:{flex:1,overflowY:"auto",minWidth:0,margin:"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-surface-default)",...s},children:n})]})]})}const O={fontFamilyBase:'"DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',fontFamilyMono:'"DM Mono", "Fira Code", "Cascadia Code", monospace',fontFamilyDisplay:'"Georama", "DM Sans", sans-serif',fontSizeXs:"0.75rem",fontSizeSm:"0.875rem",fontSizeMd:"1rem",fontSizeLg:"1.125rem",fontSizeXl:"1.25rem",fontSize2xl:"1.5rem",fontSize3xl:"1.875rem",fontWeightRegular:"400",fontWeightMedium:"500",fontWeightSemibold:"600",fontWeightBold:"700",lineHeightTight:"1.25",lineHeightBase:"1.5",lineHeightRelaxed:"1.75",letterSpacingTight:"-0.02em",letterSpacingBase:"0em",letterSpacingWide:"0.04em"},H={space0:"0px",space1:"0.25rem",space2:"0.5rem",space3:"0.75rem",space4:"1rem",space5:"1.25rem",space6:"1.5rem",space8:"2rem",space10:"2.5rem",space12:"3rem",space16:"4rem",space20:"5rem",space24:"6rem"},G={radiusNone:"0px",radiusSm:"0.25rem",radiusMd:"0.375rem",radiusLg:"0.5rem",radiusXl:"0.75rem",radiusFull:"9999px"},U={durationFast:"100ms",durationBase:"200ms",durationSlow:"350ms",easingDefault:"cubic-bezier(0.4, 0, 0.2, 1)",easingEmphasized:"cubic-bezier(0.2, 0, 0, 1)",easingDecelerate:"cubic-bezier(0, 0, 0.2, 1)"},Ft={shadowNone:"none",shadowSm:"0 1px 2px 0 rgb(0 0 0 / 0.05)",shadowMd:"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",shadowLg:"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",shadowXl:"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"},At={shadowNone:"none",shadowSm:"0 1px 2px 0 rgb(0 0 0 / 0.3)",shadowMd:"0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4)",shadowLg:"0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.4)",shadowXl:"0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.5)"},R={...O,...H,...G,...U,...Ft,bgBase:"#ffffff",bgSubtle:"#f9fafb",bgMuted:"#f3f4f6",bgOverlay:"rgb(0 0 0 / 0.4)",surfaceDefault:"#ffffff",surfaceRaised:"#ffffff",surfaceOverlay:"#ffffff",borderDefault:"#e5e7eb",borderSubtle:"#f3f4f6",borderStrong:"#9ca3af",textPrimary:"#111827",textSecondary:"#6b7280",textDisabled:"#9ca3af",textInverse:"#ffffff",textOnAccent:"#ffffff",accentDefault:"#111827",accentHover:"#1f2937",accentActive:"#374151",accentSubtle:"#f3f4f6",successDefault:"#16a34a",successSubtle:"#f0fdf4",successText:"#15803d",warningDefault:"#d97706",warningSubtle:"#fffbeb",warningText:"#b45309",dangerDefault:"#dc2626",dangerHover:"#b91c1c",dangerSubtle:"#fef2f2",dangerText:"#b91c1c",infoDefault:"#2563eb",infoSubtle:"#eff6ff",infoText:"#1d4ed8",focusRing:"#111827"},_={...O,...H,...G,...U,...At,bgBase:"#0b0d12",bgSubtle:"#111318",bgMuted:"#1c1f2a",bgOverlay:"rgb(0 0 0 / 0.6)",surfaceDefault:"#111318",surfaceRaised:"#1c1f2a",surfaceOverlay:"#1c1f2a",borderDefault:"#1c1f2a",borderSubtle:"#16181f",borderStrong:"#4a4d57",textPrimary:"#f0ede6",textSecondary:"#9ca3af",textDisabled:"#4a4d57",textInverse:"#0b0d12",textOnAccent:"#111827",accentDefault:"#f9fafb",accentHover:"#e5e7eb",accentActive:"#d1d5db",accentSubtle:"rgb(249 250 251 / 0.1)",successDefault:"#22c55e",successSubtle:"rgb(34 197 94 / 0.1)",successText:"#4ade80",warningDefault:"#f59e0b",warningSubtle:"rgb(245 158 11 / 0.1)",warningText:"#fbbf24",dangerDefault:"#ef4444",dangerHover:"#f87171",dangerSubtle:"rgb(239 68 68 / 0.1)",dangerText:"#f87171",infoDefault:"#3b82f6",infoSubtle:"rgb(59 130 246 / 0.1)",infoText:"#60a5fa",focusRing:"#f9fafb"};function Nt(n){return"--lucent-"+n.replace(/([A-Z])/g,t=>`-${t.toLowerCase()}`).replace(/([a-z])(\d)/g,(t,a,r)=>`${a}-${r}`)}function K(n,t=":root"){const a=Object.entries(n).map(([r,o])=>` ${Nt(r)}: ${o};`).join(`
|
|
107
112
|
`);return`${t} {
|
|
108
113
|
${a}
|
|
109
|
-
}`}function
|
|
110
|
-
`+
|
|
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
|
+
`+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(`
|
|
111
116
|
`);throw new Error(`Invalid ComponentManifest:
|
|
112
|
-
${a}`)}}function
|
|
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;
|