release: WRNexusJS 0.2.48

This commit is contained in:
2026-07-18 19:07:23 +05:30
parent 95ca1c0625
commit 4a3ee3baa7
79 changed files with 213 additions and 202 deletions
+1
View File
@@ -0,0 +1 @@
component Accordion { props { title = "Question" open = false } state expanded = open view { <div class="border-b border-slate-200 dark:border-slate-800"><button type="button" class="flex w-full items-center justify-between gap-4 py-5 text-left font-semibold" aria-expanded="{expanded}" @click="expanded = !expanded"><span>{title}</span><span class="text-xl transition-transform {expanded ? 'rotate-45' : ''}">+</span></button><div data-show="expanded" class="pb-5 text-slate-600 dark:text-slate-300"><slot /></div></div> } }
@@ -0,0 +1 @@
component ApiEndpointCard { props { method = "GET" path = "/api/example" description = "" } view { <article class="rounded-2xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900"><div class="flex flex-wrap items-center gap-3"><span class="rounded-lg px-2.5 py-1 font-mono text-xs font-bold {method === 'GET' ? 'bg-emerald-100 text-emerald-700' : method === 'POST' ? 'bg-blue-100 text-blue-700' : method === 'DELETE' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}">{method}</span><code class="text-sm font-semibold">{path}</code></div><p data-show="description" class="mt-3 text-sm text-slate-500">{description}</p><div class="mt-4"><slot /></div></article> } }
+1
View File
@@ -0,0 +1 @@
component Banner { props { text = "Announcement" href = "" actionLabel = "Learn more" variant = "brand" } view { <aside class="px-4 py-3 text-center text-sm font-medium {variant === 'warning' ? 'bg-amber-400 text-amber-950' : 'bg-violet-600 text-white'}"><span>{text}</span><a data-show="href" href="{href}" class="ml-2 underline underline-offset-4">{actionLabel} →</a></aside> } }
+1
View File
@@ -0,0 +1 @@
component Breadcrumbs { props { homeLabel = "Home" homeHref = "/" current = "Current page" } view { <nav aria-label="Breadcrumb"><ol class="flex flex-wrap items-center gap-2 text-sm"><li><a href="{homeHref}" class="text-slate-500 hover:text-violet-600">{homeLabel}</a></li><li aria-hidden="true" class="text-slate-400">/</li><li aria-current="page" class="font-medium text-slate-900 dark:text-white">{current}</li></ol></nav> } }
+1
View File
@@ -0,0 +1 @@
component CTASection { props { eyebrow = "Ready?" title = "Start building today" description = "" primaryLabel = "Get started" primaryHref = "#" secondaryLabel = "Contact sales" secondaryHref = "#" } view { <section class="overflow-hidden rounded-3xl bg-gradient-to-br from-violet-600 via-violet-700 to-fuchsia-700 px-6 py-12 text-center text-white shadow-2xl sm:px-12 sm:py-16"><p class="text-sm font-bold uppercase tracking-[0.18em] text-violet-100">{eyebrow}</p><h2 class="mx-auto mt-3 max-w-3xl text-3xl font-black sm:text-4xl">{title}</h2><p data-show="description" class="mx-auto mt-4 max-w-2xl text-violet-100">{description}</p><div class="mt-8 flex flex-wrap justify-center gap-3"><a href="{primaryHref}" class="inline-flex min-h-12 items-center rounded-xl bg-white px-5 font-semibold text-violet-700 shadow-lg transition hover:-translate-y-0.5">{primaryLabel}</a><a data-show="secondaryLabel" href="{secondaryHref}" class="inline-flex min-h-12 items-center rounded-xl border border-white/30 px-5 font-semibold text-white transition hover:bg-white/10">{secondaryLabel}</a></div></section> } }
+1
View File
@@ -0,0 +1 @@
component Cluster { props { gap = "4" className = "" } view { <div class="flex flex-wrap items-center gap-{gap} {className}"><slot /></div> } }
+1
View File
@@ -0,0 +1 @@
component CodeBlock { props { language = "text" filename = "" code = "" } state copied = false view { <div class="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 text-slate-100"><div class="flex min-h-11 items-center justify-between border-b border-slate-800 px-4 text-xs text-slate-400"><span>{filename || language}</span><button type="button" @click="navigator.clipboard.writeText(code); copied = true" class="rounded-lg px-2 py-1 hover:bg-slate-800">{copied ? 'Copied' : 'Copy'}</button></div><pre class="overflow-x-auto p-4 text-sm leading-6"><code>{code}</code></pre></div> } }
+1
View File
@@ -0,0 +1 @@
component CookieBanner { props { title = "We use cookies" description = "We use essential cookies and optional analytics to improve your experience." privacyHref = "/privacy" } state visible = true view { <aside data-show="visible" class="fixed inset-x-4 bottom-4 z-50 mx-auto max-w-4xl rounded-3xl border border-slate-200 bg-white/95 p-5 shadow-2xl backdrop-blur-xl dark:border-slate-800 dark:bg-slate-900/95" role="dialog" aria-label="Cookie preferences"><div class="flex flex-col gap-5 md:flex-row md:items-center"><div class="min-w-0 flex-1"><h2 class="font-bold">{title}</h2><p class="mt-1 text-sm text-slate-500">{description} <a href="{privacyHref}" class="text-violet-600 underline">Privacy policy</a></p></div><div class="flex flex-wrap gap-2"><button @click="visible = false" class="min-h-11 rounded-xl border border-slate-300 px-4 text-sm font-semibold dark:border-slate-700">Reject optional</button><button @click="visible = false" class="min-h-11 rounded-xl bg-violet-600 px-4 text-sm font-semibold text-white">Accept all</button></div></div></aside> } }
+11
View File
@@ -0,0 +1,11 @@
component DateInput {
props { id = "" name = "" label = "Date" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="date" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+1
View File
@@ -0,0 +1 @@
component DatePicker { props { id = "date" name = "date" label = "Date" value = "" min = "" max = "" required = false } view { <DateInput id="{id}" name="{name}" label="{label}" value="{value}" required="{required}" /> } }
@@ -0,0 +1 @@
component DateRangePicker { props { label = "Date range" startName = "startDate" endName = "endDate" } view { <fieldset><legend class="mb-2 text-sm font-semibold">{label}</legend><div class="grid gap-3 sm:grid-cols-2"><DateInput id="{startName}" name="{startName}" label="Start date" /><DateInput id="{endName}" name="{endName}" label="End date" /></div></fieldset> } }
+11
View File
@@ -0,0 +1,11 @@
component DateTimeInput {
props { id = "" name = "" label = "DateTime" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="datetime-local" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+1
View File
@@ -0,0 +1 @@
component Drawer { props { title = "Panel" open = false side = "right" } state visible = open view { <div data-show="visible" class="fixed inset-0 z-50" role="dialog" aria-modal="true"><button class="absolute inset-0 bg-slate-950/50" aria-label="Close" @click="visible = false"></button><aside class="absolute inset-y-0 w-[min(92vw,28rem)] overflow-auto bg-white p-6 shadow-2xl transition dark:bg-slate-900 {side === 'left' ? 'left-0' : 'right-0'}"><header class="flex items-center justify-between"><h2 class="text-xl font-bold">{title}</h2><button @click="visible = false" class="size-10 rounded-xl hover:bg-slate-100">×</button></header><div class="mt-6"><slot /></div></aside></div> } }
+1
View File
@@ -0,0 +1 @@
component DropdownMenu { props { label = "Menu" } state open = false view { <div class="relative inline-block"><button type="button" aria-haspopup="menu" aria-expanded="{open}" @click="open = !open" class="inline-flex min-h-11 items-center gap-2 rounded-xl border border-slate-300 px-4 text-sm font-semibold dark:border-slate-700">{label}<span aria-hidden="true">⌄</span></button><div data-show="open" role="menu" class="absolute right-0 z-30 mt-2 min-w-48 rounded-xl border border-slate-200 bg-white p-1.5 shadow-xl dark:border-slate-800 dark:bg-slate-900"><slot /></div></div> } }
+11
View File
@@ -0,0 +1,11 @@
component EmailInput {
props { id = "" name = "" label = "Email" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="email" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+1
View File
@@ -0,0 +1 @@
component EmptyState { props { icon = "◇" title = "Nothing here yet" description = "" actionLabel = "" actionHref = "#" } view { <div class="rounded-3xl border border-dashed border-slate-300 px-6 py-14 text-center dark:border-slate-700"><div class="mx-auto flex size-14 items-center justify-center rounded-2xl bg-violet-100 text-2xl text-violet-700 dark:bg-violet-950 dark:text-violet-300">{icon}</div><h3 class="mt-4 text-lg font-bold">{title}</h3><p data-show="description" class="mx-auto mt-2 max-w-md text-sm text-slate-500">{description}</p><a data-show="actionLabel" href="{actionHref}" class="mt-5 inline-flex min-h-11 items-center rounded-xl bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white">{actionLabel}</a></div> } }
+1
View File
@@ -0,0 +1 @@
component ErrorState { props { code = "500" title = "Something went wrong" description = "Please try again." actionLabel = "Try again" actionHref = "/" } view { <div class="mx-auto max-w-2xl px-4 py-24 text-center"><div class="text-8xl font-black text-violet-100 dark:text-violet-950">{code}</div><h1 class="-mt-4 text-3xl font-black">{title}</h1><p class="mt-4 text-slate-500">{description}</p><a href="{actionHref}" class="mt-8 inline-flex min-h-12 items-center rounded-xl bg-violet-600 px-5 font-semibold text-white">{actionLabel}</a></div> } }
+1
View File
@@ -0,0 +1 @@
component FeatureCard { props { icon = "✦" title = "Feature" description = "" href = "" } view { <article class="group rounded-2xl border border-slate-200 bg-white p-6 transition duration-300 hover:-translate-y-1 hover:border-violet-300 hover:shadow-xl dark:border-slate-800 dark:bg-slate-900"><div class="flex size-11 items-center justify-center rounded-xl bg-violet-100 text-xl text-violet-700 transition group-hover:scale-110 dark:bg-violet-950 dark:text-violet-300">{icon}</div><h3 class="mt-5 text-lg font-bold">{title}</h3><p class="mt-2 leading-7 text-slate-600 dark:text-slate-300">{description}</p><a data-show="href" href="{href}" class="mt-4 inline-flex font-semibold text-violet-600">Learn more →</a></article> } }
+1
View File
@@ -0,0 +1 @@
component FeatureGrid { props { columns = 3 className = "" } view { <div class="grid gap-5 {columns === 2 ? 'md:grid-cols-2' : columns === 4 ? 'sm:grid-cols-2 xl:grid-cols-4' : 'md:grid-cols-2 lg:grid-cols-3'} {className}"><slot /></div> } }
+1
View File
@@ -0,0 +1 @@
component FileUpload { props { id = "file" name = "file" label = "Upload file" accept = "" multiple = false help = "Drag and drop or browse" } view { <label for="{id}" class="flex min-h-40 cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-slate-300 bg-slate-50 p-6 text-center transition hover:border-violet-500 hover:bg-violet-50/50 focus-within:ring-4 focus-within:ring-violet-500/10 dark:border-slate-700 dark:bg-slate-900/50"><span class="text-3xl" aria-hidden="true">⇧</span><span class="mt-2 font-semibold">{label}</span><span class="mt-1 text-sm text-slate-500">{help}</span><input id="{id}" name="{name}" type="file" accept="{accept}" multiple="{multiple}" class="sr-only" /></label> } }
@@ -0,0 +1 @@
component FormErrorSummary { props { title = "Please fix the following" visible = true } view { <section data-show="visible" role="alert" aria-live="polite" class="rounded-2xl border border-red-200 bg-red-50 p-4 text-red-900 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200"><h3 class="font-semibold">{title}</h3><div class="mt-2 text-sm"><slot /></div></section> } }
+1
View File
@@ -0,0 +1 @@
component FormField { props { label = "Field" help = "" error = "" required = false } view { <div class="space-y-2"><div class="text-sm font-semibold">{label}<span data-show="required" class="ml-1 text-red-500">*</span></div><slot /><p data-show="help && !error" class="text-sm text-slate-500">{help}</p><p data-show="error" role="alert" class="text-sm font-medium text-red-600">{error}</p></div> } }
+1
View File
@@ -0,0 +1 @@
component Hero { props { eyebrow = "" title = "Build faster with WRNexusJS" highlight = "" description = "" primaryLabel = "Get started" primaryHref = "#" secondaryLabel = "Learn more" secondaryHref = "#" align = "center" } view { <section class="relative isolate overflow-hidden py-20 sm:py-28"><div class="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top,rgba(124,58,237,.18),transparent_45%)]"></div><div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"><div class="max-w-4xl {align === 'center' ? 'mx-auto text-center' : ''}"><Badge data-show="eyebrow" label="{eyebrow}" variant="brand" /><h1 class="mt-6 text-4xl font-black tracking-tight text-slate-950 sm:text-6xl dark:text-white">{title} <span data-show="highlight" class="bg-gradient-to-r from-violet-600 to-fuchsia-500 bg-clip-text text-transparent">{highlight}</span></h1><p data-show="description" class="mt-6 text-lg leading-8 text-slate-600 sm:text-xl dark:text-slate-300">{description}</p><div class="mt-8 flex flex-wrap gap-3 {align === 'center' ? 'justify-center' : ''}"><a href="{primaryHref}" class="inline-flex min-h-12 items-center rounded-xl bg-violet-600 px-5 font-semibold text-white shadow-lg shadow-violet-600/20 transition hover:-translate-y-0.5 hover:bg-violet-700">{primaryLabel}</a><a data-show="secondaryLabel" href="{secondaryHref}" class="inline-flex min-h-12 items-center rounded-xl border border-slate-300 bg-white px-5 font-semibold text-slate-900 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-white">{secondaryLabel}</a></div><div class="mt-10"><slot /></div></div></div></section> } }
+4
View File
@@ -0,0 +1,4 @@
component IconButton {
props { label = "Action" icon = "•" variant = "ghost" disabled = false }
view { <button type="button" aria-label="{label}" title="{label}" disabled="{disabled}" class="inline-flex size-11 items-center justify-center rounded-xl transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 disabled:opacity-50 {variant === 'primary' ? 'bg-violet-600 text-white hover:bg-violet-700' : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'}"><span aria-hidden="true">{icon}</span></button> }
}
+1
View File
@@ -0,0 +1 @@
component Inline { props { gap = "4" className = "" } view { <div class="flex flex-wrap items-center gap-{gap} {className}"><slot /></div> } }
@@ -0,0 +1 @@
component LegalDocumentLayout { props { title = "Legal document" effectiveDate = "" updatedDate = "" } view { <article class="mx-auto grid max-w-7xl gap-10 px-4 py-14 lg:grid-cols-[16rem_minmax(0,1fr)] lg:px-8"><aside class="lg:sticky lg:top-24 lg:self-start"><div class="text-sm font-semibold">On this page</div><nav class="mt-4 text-sm text-slate-500"><slot name="toc" /></nav></aside><main class="min-w-0 max-w-3xl"><header class="border-b border-slate-200 pb-8 dark:border-slate-800"><h1 class="text-4xl font-black tracking-tight">{title}</h1><div class="mt-4 flex flex-wrap gap-4 text-sm text-slate-500"><span data-show="effectiveDate">Effective: {effectiveDate}</span><span data-show="updatedDate">Updated: {updatedDate}</span></div></header><div class="prose prose-slate mt-8 max-w-none dark:prose-invert"><slot /></div></main></article> } }
+1
View File
@@ -0,0 +1 @@
component Link { props { href = "#" label = "Link" external = false className = "" } view { <a href="{href}" target="{external ? '_blank' : ''}" rel="{external ? 'noreferrer' : ''}" class="font-medium text-violet-600 underline-offset-4 transition hover:text-violet-700 hover:underline focus-visible:rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 {className}">{label}<span data-show="external" aria-hidden="true"> ↗</span></a> } }
+1
View File
@@ -0,0 +1 @@
component LogoCloud { props { title = "Trusted by modern teams" } view { <section aria-label="{title}"><p class="text-center text-sm font-semibold uppercase tracking-wider text-slate-500">{title}</p><div class="mt-6 flex flex-wrap items-center justify-center gap-x-10 gap-y-6 opacity-70 grayscale transition hover:grayscale-0"><slot /></div></section> } }
+1
View File
@@ -0,0 +1 @@
component Modal { props { title = "Dialog" description = "" open = false size = "md" closeLabel = "Close" } state visible = open view { <div data-show="visible" class="fixed inset-0 z-50 flex items-end justify-center p-4 sm:items-center" role="dialog" aria-modal="true" aria-labelledby="modal-title"><button class="absolute inset-0 bg-slate-950/60 backdrop-blur-sm" aria-label="{closeLabel}" @click="visible = false"></button><section class="relative max-h-[90vh] w-full overflow-auto rounded-3xl border border-slate-200 bg-white p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900 {size === 'sm' ? 'max-w-md' : size === 'lg' ? 'max-w-4xl' : 'max-w-2xl'}"><header class="flex items-start justify-between gap-4"><div><h2 id="modal-title" class="text-xl font-bold">{title}</h2><p data-show="description" class="mt-1 text-sm text-slate-500">{description}</p></div><button type="button" aria-label="{closeLabel}" @click="visible = false" class="inline-flex size-10 items-center justify-center rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800">×</button></header><div class="mt-5"><slot /></div></section></div> } }
+11
View File
@@ -0,0 +1,11 @@
component NumberInput {
props { id = "" name = "" label = "Number" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="number" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+1
View File
@@ -0,0 +1 @@
component OtpInput { props { name = "otp" length = 6 label = "Verification code" } view { <fieldset><legend class="mb-3 text-sm font-semibold">{label}</legend><input name="{name}" inputmode="numeric" autocomplete="one-time-code" maxlength="{length}" pattern="[0-9]*" class="min-h-14 w-full rounded-2xl border border-slate-300 bg-white px-4 text-center font-mono text-2xl tracking-[0.5em] outline-none transition focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 dark:border-slate-700 dark:bg-slate-950" /></fieldset> } }
+1
View File
@@ -0,0 +1 @@
component PageShell { props { className = "" } view { <div class="min-h-screen bg-white text-slate-950 antialiased selection:bg-violet-200 dark:bg-slate-950 dark:text-white dark:selection:bg-violet-800 {className}"><slot /></div> } }
+1
View File
@@ -0,0 +1 @@
component Pagination { props { page = 1 totalPages = 1 previousHref = "#" nextHref = "#" } view { <nav aria-label="Pagination" class="flex items-center justify-between gap-4"><a href="{previousHref}" aria-disabled="{page <= 1}" class="inline-flex min-h-11 items-center rounded-xl border border-slate-300 px-4 text-sm font-semibold aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:border-slate-700">← Previous</a><span class="text-sm text-slate-500">Page {page} of {totalPages}</span><a href="{nextHref}" aria-disabled="{page >= totalPages}" class="inline-flex min-h-11 items-center rounded-xl border border-slate-300 px-4 text-sm font-semibold aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:border-slate-700">Next →</a></nav> } }
+11
View File
@@ -0,0 +1,11 @@
component PasswordInput {
props { id = "" name = "" label = "Password" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="password" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+11
View File
@@ -0,0 +1,11 @@
component PhoneInput {
props { id = "" name = "" label = "Phone" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="tel" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
@@ -0,0 +1 @@
component PricingPlanCard { props { name = "Starter" description = "" price = "₹0" period = "/month" featured = false ctaLabel = "Choose plan" ctaHref = "#" } view { <article class="relative flex h-full flex-col rounded-3xl border p-6 shadow-sm {featured ? 'border-violet-500 bg-violet-50/50 ring-2 ring-violet-500 dark:bg-violet-950/20' : 'border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'}"><Badge data-show="featured" label="Most popular" variant="brand" /><h3 class="mt-4 text-xl font-bold">{name}</h3><p class="mt-2 text-sm text-slate-500">{description}</p><div class="mt-6"><span class="text-4xl font-black">{price}</span><span class="text-slate-500">{period}</span></div><div class="mt-6 flex-1"><slot /></div><a href="{ctaHref}" class="mt-6 inline-flex min-h-12 items-center justify-center rounded-xl font-semibold transition {featured ? 'bg-violet-600 text-white hover:bg-violet-700' : 'border border-slate-300 hover:bg-slate-50 dark:border-slate-700'}">{ctaLabel}</a></article> } }
+1
View File
@@ -0,0 +1 @@
component ProgressBar { props { value = 0 max = 100 label = "Progress" showValue = true } view { <div><div class="mb-2 flex justify-between text-sm"><span>{label}</span><span data-show="showValue">{value}%</span></div><div class="h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800" role="progressbar" aria-valuenow="{value}" aria-valuemin="0" aria-valuemax="{max}"><div class="h-full rounded-full bg-violet-600 transition-all duration-500" style="width: {value}%"></div></div></div> } }
+1
View File
@@ -0,0 +1 @@
component PublicFooter { props { brand = "WRNexusJS" description = "SSR-first, Bun-native full-stack framework." copyright = "© 2026 WorkRoot Workspace" } view { <footer class="border-t border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950"><div class="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8"><div class="grid gap-10 lg:grid-cols-[1.2fr_2fr]"><div><div class="text-xl font-black">{brand}</div><p class="mt-3 max-w-sm text-sm leading-6 text-slate-500">{description}</p></div><div class="grid grid-cols-2 gap-8 sm:grid-cols-4"><slot /></div></div><div class="mt-12 flex flex-col gap-4 border-t border-slate-200 pt-6 text-sm text-slate-500 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800"><span>{copyright}</span><button data-wire-theme-toggle type="button" class="min-h-10 rounded-xl border border-slate-300 px-3 dark:border-slate-700">Toggle theme</button></div></div></footer> } }
+1
View File
@@ -0,0 +1 @@
component PublicHeader { props { brand = "WRNexusJS" homeHref = "/" signInHref = "/signin" ctaHref = "/start" ctaLabel = "Get started" } state mobileOpen = false view { <header class="sticky top-0 z-40 border-b border-slate-200/80 bg-white/85 backdrop-blur-xl dark:border-slate-800 dark:bg-slate-950/85"><div class="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"><a href="{homeHref}" class="text-lg font-black tracking-tight">{brand}</a><nav class="hidden items-center gap-7 md:flex" aria-label="Primary"><slot /></nav><div class="hidden items-center gap-3 md:flex"><a href="{signInHref}" class="text-sm font-semibold">Sign in</a><a href="{ctaHref}" class="inline-flex min-h-10 items-center rounded-xl bg-violet-600 px-4 text-sm font-semibold text-white">{ctaLabel}</a><button data-wire-theme-toggle type="button" aria-label="Toggle theme" class="size-10 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800">◐</button></div><button type="button" aria-label="Open menu" aria-expanded="{mobileOpen}" @click="mobileOpen = !mobileOpen" class="size-11 rounded-xl md:hidden">☰</button></div><div data-show="mobileOpen" class="border-t border-slate-200 px-4 py-5 md:hidden dark:border-slate-800"><div class="space-y-3"><slot name="mobile" /></div><div class="mt-5 grid grid-cols-2 gap-3"><a href="{signInHref}" class="inline-flex min-h-11 items-center justify-center rounded-xl border border-slate-300 font-semibold">Sign in</a><a href="{ctaHref}" class="inline-flex min-h-11 items-center justify-center rounded-xl bg-violet-600 font-semibold text-white">{ctaLabel}</a></div></div></header> } }
+1
View File
@@ -0,0 +1 @@
component RadioGroup { props { label = "Choose one" name = "choice" } view { <fieldset class="space-y-3"><legend class="text-sm font-semibold text-slate-900 dark:text-white">{label}</legend><div class="space-y-2"><slot /></div></fieldset> } }
@@ -0,0 +1 @@
component SchedulePicker { props { label = "Schedule" } view { <fieldset class="space-y-4 rounded-2xl border border-slate-200 p-5 dark:border-slate-800"><legend class="px-2 font-semibold">{label}</legend><DateTimeInput id="scheduleAt" name="scheduleAt" label="Date and time" /><TimezoneSelector /></fieldset> } }
+11
View File
@@ -0,0 +1,11 @@
component SearchInput {
props { id = "" name = "" label = "Search" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="search" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+6
View File
@@ -0,0 +1,6 @@
component Section {
props { id = "" size = "default" surface = "default" className = "" }
view {
<section id="{id}" class="{size === 'compact' ? 'py-10 sm:py-14' : size === 'spacious' ? 'py-20 sm:py-28' : 'py-14 sm:py-20'} {surface === 'muted' ? 'bg-slate-50 dark:bg-slate-900/50' : surface === 'brand' ? 'bg-violet-600 text-white' : ''} {className}"><slot /></section>
}
}
+1
View File
@@ -0,0 +1 @@
component SectionHeader { props { eyebrow = "" title = "Section title" description = "" align = "left" } view { <header class="max-w-3xl {align === 'center' ? 'mx-auto text-center' : ''}"><p data-show="eyebrow" class="mb-3 text-sm font-bold uppercase tracking-[0.18em] text-violet-600 dark:text-violet-400">{eyebrow}</p><h2 class="text-3xl font-bold tracking-tight text-slate-950 sm:text-4xl dark:text-white">{title}</h2><p data-show="description" class="mt-4 text-lg leading-8 text-slate-600 dark:text-slate-300">{description}</p><div class="mt-6"><slot /></div></header> } }
+1
View File
@@ -0,0 +1 @@
component StatCard { props { label = "Metric" value = "0" change = "" trend = "neutral" } view { <article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900"><p class="text-sm font-medium text-slate-500">{label}</p><div class="mt-2 flex items-end justify-between gap-3"><p class="text-3xl font-bold tracking-tight">{value}</p><span data-show="change" class="text-sm font-semibold {trend === 'up' ? 'text-emerald-600' : trend === 'down' ? 'text-red-600' : 'text-slate-500'}">{trend === 'up' ? '↑' : trend === 'down' ? '↓' : ''} {change}</span></div></article> } }
+1
View File
@@ -0,0 +1 @@
component StatusBadge { props { status = "operational" } view { <span class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold {status === 'operational' ? 'bg-emerald-100 text-emerald-700' : status === 'degraded' ? 'bg-amber-100 text-amber-700' : status === 'outage' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'}"><span class="size-2 rounded-full bg-current"></span>{status}</span> } }
+1
View File
@@ -0,0 +1 @@
component Tabs { props { active = "first" } state current = active view { <div><div role="tablist" class="flex gap-1 overflow-x-auto rounded-xl bg-slate-100 p-1 dark:bg-slate-800"><button role="tab" aria-selected="{current === 'first'}" @click="current = 'first'" class="min-h-10 flex-1 rounded-lg px-4 text-sm font-semibold transition {current === 'first' ? 'bg-white shadow dark:bg-slate-900' : 'text-slate-500'}">First</button><button role="tab" aria-selected="{current === 'second'}" @click="current = 'second'" class="min-h-10 flex-1 rounded-lg px-4 text-sm font-semibold transition {current === 'second' ? 'bg-white shadow dark:bg-slate-900' : 'text-slate-500'}">Second</button></div><div data-show="current === 'first'" role="tabpanel" class="pt-5"><slot name="first" /></div><div data-show="current === 'second'" role="tabpanel" class="pt-5"><slot name="second" /></div></div> } }
@@ -0,0 +1 @@
component TestimonialCard { props { quote = "Great product." name = "Customer" role = "" company = "" avatar = "" } view { <figure class="rounded-3xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-800 dark:bg-slate-900"><blockquote class="text-lg leading-8 text-slate-700 dark:text-slate-200">“{quote}”</blockquote><figcaption class="mt-6 flex items-center gap-3"><Avatar src="{avatar}" initials="{name.slice(0,2)}" /><div><div class="font-semibold">{name}</div><div class="text-sm text-slate-500">{role}<span data-show="company"> · {company}</span></div></div></figcaption></figure> } }
+11
View File
@@ -0,0 +1,11 @@
component TextInput {
props { id = "" name = "" label = "Text" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="text" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+11
View File
@@ -0,0 +1,11 @@
component TimeInput {
props { id = "" name = "" label = "Time" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="time" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
+1
View File
@@ -0,0 +1 @@
component Timeline { props { title = "Timeline" } view { <section><h2 class="text-lg font-bold">{title}</h2><ol class="mt-5 border-l border-slate-200 pl-6 dark:border-slate-800"><slot /></ol></section> } }
+1
View File
@@ -0,0 +1 @@
component TimelineItem { props { title = "Event" date = "" description = "" status = "default" } view { <li class="relative pb-8 last:pb-0"><span class="absolute -left-[1.85rem] top-1 size-3 rounded-full ring-4 ring-white dark:ring-slate-950 {status === 'success' ? 'bg-emerald-500' : status === 'warning' ? 'bg-amber-500' : 'bg-violet-500'}"></span><div class="flex flex-wrap items-baseline justify-between gap-2"><h3 class="font-semibold">{title}</h3><time class="text-xs text-slate-500">{date}</time></div><p data-show="description" class="mt-1 text-sm text-slate-500">{description}</p></li> } }
@@ -0,0 +1 @@
component TimezoneSelector { props { id = "timezone" name = "timezone" label = "Timezone" } view { <Select id="{id}" name="{name}" label="{label}"><option value="UTC">UTC</option><option value="Asia/Kolkata">Asia/Kolkata</option><option value="America/New_York">America/New_York</option><option value="Europe/London">Europe/London</option></Select> } }
+1
View File
@@ -0,0 +1 @@
component Toast { props { title = "Saved" description = "" variant = "success" duration = 5000 } state visible = true view { <div data-show="visible" role="status" aria-live="polite" class="pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-2xl border bg-white p-4 shadow-2xl dark:border-slate-800 dark:bg-slate-900"><span class="mt-0.5 flex size-8 items-center justify-center rounded-full {variant === 'success' ? 'bg-emerald-100 text-emerald-700' : variant === 'danger' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'}">{variant === 'success' ? '✓' : variant === 'danger' ? '!' : 'i'}</span><div class="min-w-0 flex-1"><p class="font-semibold">{title}</p><p data-show="description" class="mt-1 text-sm text-slate-500">{description}</p></div><button type="button" aria-label="Close" @click="visible = false" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100">×</button></div> } }
+11
View File
@@ -0,0 +1,11 @@
component UrlInput {
props { id = "" name = "" label = "Url" value = "" placeholder = "" help = "" error = "" required = false disabled = false readonly = false autocomplete = "" }
view {
<div class="space-y-2">
<label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label>
<input id="{id}" name="{name}" type="url" value="{value}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" readonly="{readonly}" autocomplete="{autocomplete}" aria-invalid="{!!error}" aria-describedby="{id}-help {id}-error" class="min-h-11 w-full rounded-xl border bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition duration-200 placeholder:text-slate-400 focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:opacity-70 dark:bg-slate-950 dark:text-white {error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/10' : 'border-slate-300 dark:border-slate-700'}" />
<p id="{id}-help" data-show="help && !error" class="text-sm text-slate-500 dark:text-slate-400">{help}</p>
<p id="{id}-error" data-show="error" role="alert" class="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
</div>
}
}
@@ -0,0 +1 @@
component VisuallyHidden { props { className = "" } view { <div class="sr-only {className}"><slot /></div> } }
+1 -16
View File
@@ -1,16 +1 @@
// Callout box. variant: info | success | danger | warning.
// Optional `title`; body from `message` prop or the slot.
component Alert {
props {
variant = "info"
title = ""
message = ""
class = ""
}
view {
<div class="wire-alert wire-alert--{variant} {class}" role="alert">
<div class="wire-alert__title">{title}</div>
<div class="wire-alert__body"><slot>{message}</slot></div>
</div>
}
}
component Alert { props { title = "Notice" description = "" variant = "info" dismissible = false } state visible = true view { <div data-show="visible" role="alert" class="flex items-start gap-3 rounded-2xl border p-4 {variant === 'success' ? 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-200' : variant === 'warning' ? 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-200' : variant === 'danger' ? 'border-red-200 bg-red-50 text-red-900 dark:border-red-900 dark:bg-red-950/40 dark:text-red-200' : 'border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-900 dark:bg-blue-950/40 dark:text-blue-200'}"><div class="min-w-0 flex-1"><h3 class="font-semibold">{title}</h3><p data-show="description" class="mt-1 text-sm opacity-80">{description}</p><div class="mt-2"><slot /></div></div><button data-show="dismissible" type="button" aria-label="Dismiss" @click="visible = false" class="rounded-lg p-1 hover:bg-black/5">×</button></div> } }
+1 -11
View File
@@ -1,11 +1 @@
// Rounded user image.
component Avatar {
props {
src = ""
alt = ""
class = ""
}
view {
<img class="wire-avatar {class}" src="{src}" alt="{alt}">
}
}
component Avatar { props { src = "" alt = "" initials = "WR" size = "md" } view { <span class="inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 font-semibold text-white {size === 'sm' ? 'size-8 text-xs' : size === 'lg' ? 'size-14 text-base' : 'size-10 text-sm'}"><img data-show="src" src="{src}" alt="{alt}" class="h-full w-full object-cover" /><span data-show="!src" aria-hidden="true">{initials}</span></span> } }
+1 -11
View File
@@ -1,11 +1 @@
// Small status label. variant: default | primary | success | danger | warning.
component Badge {
props {
label = ""
variant = "default"
class = ""
}
view {
<span class="wire-badge wire-badge--{variant} {class}"><slot>{label}</slot></span>
}
}
component Badge { props { label = "Badge" variant = "neutral" } view { <span class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset {variant === 'success' ? 'bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-950/40 dark:text-emerald-300' : variant === 'warning' ? 'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-950/40 dark:text-amber-300' : variant === 'danger' ? 'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-950/40 dark:text-red-300' : variant === 'brand' ? 'bg-violet-50 text-violet-700 ring-violet-600/20 dark:bg-violet-950/40 dark:text-violet-300' : 'bg-slate-100 text-slate-700 ring-slate-600/10 dark:bg-slate-800 dark:text-slate-300'}">{label}</span> } }
+5 -10
View File
@@ -1,14 +1,9 @@
// Button. variant: default | primary | danger | ghost — size: sm | md | lg.
// Use the `label` prop, or put custom content in the slot.
component Button {
props {
label = "Button"
variant = "default"
size = "md"
type = "button"
class = ""
}
props { label = "Button" type = "button" variant = "primary" size = "md" disabled = false loading = false icon = "" className = "" }
view {
<button type="{type}" class="wire-btn wire-btn--{variant} wire-btn--{size} {class}"><slot>{label}</slot></button>
<button type="{type}" disabled="{disabled || loading}" aria-busy="{loading}" class="inline-flex min-h-11 items-center justify-center gap-2 rounded-xl font-semibold transition-all duration-200 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] {size === 'sm' ? 'px-3 py-2 text-sm' : size === 'lg' ? 'px-6 py-3.5 text-base' : 'px-4 py-2.5 text-sm'} {variant === 'secondary' ? 'border border-slate-300 bg-white text-slate-900 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-white dark:hover:bg-slate-800' : variant === 'ghost' ? 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800' : variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700' : 'bg-violet-600 text-white shadow-sm hover:-translate-y-0.5 hover:bg-violet-700 hover:shadow-md'} {className}">
<span data-show="loading" class="size-4 animate-spin rounded-full border-2 border-current border-r-transparent"></span>
<span>{label}</span>
</button>
}
}
+1 -9
View File
@@ -1,9 +1 @@
// Surface container with padding, border, and rounded corners.
component Card {
props {
class = ""
}
view {
<div class="wire-card {class}"><slot></slot></div>
}
}
component Card { props { variant = "default" padding = "md" interactive = false className = "" } view { <article class="rounded-2xl border border-slate-200 bg-white text-slate-950 shadow-sm transition duration-300 dark:border-slate-800 dark:bg-slate-900 dark:text-white {padding === 'sm' ? 'p-4' : padding === 'lg' ? 'p-8' : 'p-6'} {variant === 'glass' ? 'bg-white/75 backdrop-blur-xl dark:bg-slate-900/70' : variant === 'elevated' ? 'shadow-xl shadow-slate-950/5' : ''} {interactive ? 'hover:-translate-y-1 hover:shadow-xl' : ''} {className}"><slot /></article> } }
+1 -14
View File
@@ -1,14 +1 @@
// Checkbox with an inline label.
component Checkbox {
props {
name = ""
label = ""
class = ""
}
view {
<label class="wire-check {class}">
<input type="checkbox" name="{name}" class="wire-check__box">
<span class="wire-check__label"><slot>{label}</slot></span>
</label>
}
}
component Checkbox { props { id = "" name = "" label = "Checkbox" description = "" checked = false disabled = false } view { <label for="{id}" class="flex min-h-11 cursor-pointer items-start gap-3 rounded-xl p-2 transition hover:bg-slate-50 dark:hover:bg-slate-900"><input id="{id}" name="{name}" type="checkbox" checked="{checked}" disabled="{disabled}" class="mt-0.5 size-5 rounded border-slate-300 text-violet-600 focus:ring-violet-500 dark:border-slate-700 dark:bg-slate-950" /><span><span class="block text-sm font-semibold text-slate-900 dark:text-white">{label}</span><span data-show="description" class="mt-0.5 block text-sm text-slate-500 dark:text-slate-400">{description}</span></span></label> } }
+2 -5
View File
@@ -1,9 +1,6 @@
// Centered, max-width page container. Children go in the <slot>.
component Container {
props {
class = ""
}
props { size = "default" className = "" }
view {
<div class="wire-container {class}"><slot></slot></div>
<div class="mx-auto w-full px-4 sm:px-6 lg:px-8 {size === 'narrow' ? 'max-w-3xl' : size === 'wide' ? 'max-w-screen-2xl' : 'max-w-7xl'} {className}"><slot /></div>
}
}
+1 -9
View File
@@ -1,9 +1 @@
// Horizontal rule using theme border color.
component Divider {
props {
class = ""
}
view {
<hr class="wire-divider {class}">
}
}
component Divider { props { className = "" } view { <div class="h-px w-full bg-slate-200 dark:bg-slate-800 {className}"><slot /></div> } }
+1 -11
View File
@@ -1,11 +1 @@
// Responsive-ish grid: `cols` columns (16) with a gap (scale 08).
component Grid {
props {
cols = "2"
gap = "4"
class = ""
}
view {
<div class="wire-grid wire-cols-{cols} wire-gap-{gap} {class}"><slot></slot></div>
}
}
component Grid { props { gap = "4" className = "" } view { <div class="grid gap-{gap} {className}"><slot /></div> } }
+1 -15
View File
@@ -1,15 +1 @@
// Radio input with an inline label.
component Radio {
props {
name = ""
value = ""
label = ""
class = ""
}
view {
<label class="wire-check {class}">
<input type="radio" name="{name}" value="{value}" class="wire-check__box">
<span class="wire-check__label"><slot>{label}</slot></span>
</label>
}
}
component Radio { props { id = "" name = "choice" value = "" label = "Option" description = "" checked = false disabled = false } view { <label for="{id}" class="flex min-h-11 cursor-pointer items-start gap-3 rounded-xl border border-slate-200 p-3 transition hover:border-violet-400 dark:border-slate-800"><input id="{id}" name="{name}" type="radio" value="{value}" checked="{checked}" disabled="{disabled}" class="mt-0.5 size-5 border-slate-300 text-violet-600 focus:ring-violet-500" /><span><span class="block text-sm font-semibold">{label}</span><span data-show="description" class="text-sm text-slate-500">{description}</span></span></label> } }
+1 -10
View File
@@ -1,10 +1 @@
// Styled native select. Put <option>s in the slot.
component Select {
props {
name = ""
class = ""
}
view {
<select name="{name}" class="wire-input wire-select {class}"><slot></slot></select>
}
}
component Select { props { id = "" name = "" label = "Select" help = "" error = "" required = false disabled = false } view { <div class="space-y-2"><label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label><select id="{id}" name="{name}" required="{required}" disabled="{disabled}" class="min-h-11 w-full rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-slate-950 shadow-sm outline-none transition focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 dark:border-slate-700 dark:bg-slate-950 dark:text-white"><slot /></select><p data-show="help && !error" class="text-sm text-slate-500">{help}</p><p data-show="error" role="alert" class="text-sm font-medium text-red-600">{error}</p></div> } }
+1 -11
View File
@@ -1,11 +1 @@
// Loading placeholder (animated shimmer). Size it with width/height props.
component Skeleton {
props {
width = "100%"
height = "1rem"
class = ""
}
view {
<span class="wire-skeleton {class}" style="width: {width}; height: {height};" aria-hidden="true"></span>
}
}
component Skeleton { props { height = "4" rounded = "lg" className = "" } view { <div aria-hidden="true" class="animate-pulse bg-slate-200 dark:bg-slate-800 h-{height} rounded-{rounded} {className}"></div> } }
+1 -9
View File
@@ -1,9 +1 @@
// Loading spinner (pure CSS animation, no JS).
component Spinner {
props {
class = ""
}
view {
<span class="wire-spinner {class}" role="status" aria-label="Loading"></span>
}
}
component Spinner { props { label = "Loading" size = "md" } view { <span role="status" aria-label="{label}" class="inline-block animate-spin rounded-full border-2 border-slate-300 border-r-violet-600 {size === 'sm' ? 'size-4' : size === 'lg' ? 'size-10' : 'size-6'}"></span> } }
+1 -10
View File
@@ -1,10 +1 @@
// Vertical stack: lays children out in a column with a gap (scale 08).
component Stack {
props {
gap = "4"
class = ""
}
view {
<div class="wire-stack wire-gap-{gap} {class}"><slot></slot></div>
}
}
component Stack { props { gap = "4" className = "" } view { <div class="flex flex-col gap-{gap} {className}"><slot /></div> } }
+1 -15
View File
@@ -1,15 +1 @@
// Toggle switch (an accessible checkbox styled as a switch).
component Switch {
props {
name = ""
label = ""
class = ""
}
view {
<label class="wire-switch {class}">
<input type="checkbox" name="{name}" class="wire-switch__input" role="switch">
<span class="wire-switch__track"><span class="wire-switch__thumb"></span></span>
<span class="wire-switch__label"><slot>{label}</slot></span>
</label>
}
}
component Switch { props { id = "" name = "" label = "Switch" description = "" checked = false disabled = false } state on = checked view { <button id="{id}" type="button" role="switch" aria-checked="{on}" disabled="{disabled}" @click="on = !on" class="flex w-full items-center justify-between gap-4 rounded-xl p-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 disabled:opacity-50"><span><span class="block text-sm font-semibold">{label}</span><span data-show="description" class="text-sm text-slate-500">{description}</span></span><span class="relative inline-flex h-6 w-11 shrink-0 rounded-full transition {on ? 'bg-violet-600' : 'bg-slate-300 dark:bg-slate-700'}"><span class="absolute top-0.5 size-5 rounded-full bg-white shadow transition-transform {on ? 'translate-x-5' : 'translate-x-0.5'}"></span></span><input type="hidden" name="{name}" value="{on ? 'true' : 'false'}" /></button> } }
+1 -12
View File
@@ -1,12 +1 @@
// Themed table wrapper — put a <table>…</table> (or thead/tbody) in the slot.
// Scrolls horizontally on small screens.
component Table {
props {
class = ""
}
view {
<div class="wire-table-wrap {class}">
<table class="wire-table"><slot></slot></table>
</div>
}
}
component Table { props { caption = "Data table" responsive = true } view { <div class="w-full overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800"><table class="w-full min-w-[640px] border-collapse text-left text-sm"><caption class="sr-only">{caption}</caption><slot /></table></div> } }
+1 -12
View File
@@ -1,12 +1 @@
// Multi-line text input.
component Textarea {
props {
name = ""
placeholder = ""
rows = "4"
class = ""
}
view {
<textarea name="{name}" placeholder="{placeholder}" rows="{rows}" class="wire-input wire-textarea {class}"><slot></slot></textarea>
}
}
component Textarea { props { id = "" name = "" label = "Message" value = "" placeholder = "" rows = 5 help = "" error = "" required = false disabled = false } view { <div class="space-y-2"><label for="{id}" class="block text-sm font-semibold text-slate-800 dark:text-slate-200">{label}<span data-show="required" class="ml-1 text-red-500">*</span></label><textarea id="{id}" name="{name}" rows="{rows}" placeholder="{placeholder}" required="{required}" disabled="{disabled}" aria-invalid="{!!error}" class="w-full resize-y rounded-xl border bg-white px-3.5 py-3 text-slate-950 shadow-sm outline-none transition focus:border-violet-500 focus:ring-4 focus:ring-violet-500/10 dark:bg-slate-950 dark:text-white {error ? 'border-red-500' : 'border-slate-300 dark:border-slate-700'}">{value}</textarea><p data-show="help && !error" class="text-sm text-slate-500">{help}</p><p data-show="error" role="alert" class="text-sm font-medium text-red-600">{error}</p></div> } }
+1 -10
View File
@@ -1,10 +1 @@
// CSS-only tooltip shown on hover/focus. Wrap the trigger content in the slot.
component Tooltip {
props {
text = ""
class = ""
}
view {
<span class="wire-tooltip {class}" data-tooltip="{text}" tabindex="0"><slot></slot></span>
}
}
component Tooltip { props { text = "Helpful information" position = "top" } view { <span class="group relative inline-flex"><slot /><span role="tooltip" class="pointer-events-none absolute z-40 w-max max-w-xs rounded-lg bg-slate-950 px-2.5 py-1.5 text-xs font-medium text-white opacity-0 shadow-lg transition duration-200 group-hover:opacity-100 group-focus-within:opacity-100 {position === 'bottom' ? 'left-1/2 top-full mt-2 -translate-x-1/2' : 'bottom-full left-1/2 mb-2 -translate-x-1/2'}">{text}</span></span> } }
+1
View File
@@ -19,6 +19,7 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./components/*": "./components/*",
"./ui.css": "./ui.css"
},
"dependencies": {
+14
View File
@@ -523,3 +523,17 @@
.wire-table tbody tr:hover {
background: var(--wire-color-surface-2);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
:where(button, a, input, select, textarea):focus-visible {
outline: 2px solid rgb(124 58 237);
outline-offset: 2px;
}
+8
View File
@@ -501,6 +501,14 @@ const MIGRATIONS: Migration[] = [
// No generated application files require automatic migration.
},
},
{
version: "0.2.48",
id: "components-update",
description: "Adds reactive hydration support for server-rendered each loop locals.",
apply() {
// No generated application files require automatic migration.
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
+1
View File
@@ -6,6 +6,7 @@
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./components/*": "./components/*",
"./ui.css": "./ui.css"
},
"dependencies": {
+2 -2
View File
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { uiComponentNames, uiComponentsDir, uiCss } from "../src/index.ts";
test("bundled UI assets are discoverable and readable", () => {
expect(uiComponentNames()).toContain("button");
expect(readFileSync(join(uiComponentsDir(), "button.wrn"), "utf8")).toContain("component Button");
expect(uiComponentNames()).toContain("Button");
expect(readFileSync(join(uiComponentsDir(), "Button.wrn"), "utf8")).toContain("component Button");
expect(uiCss()).toContain("--wire-");
});