release: WRNexusJS 0.2.52

This commit is contained in:
2026-07-19 14:23:10 +05:30
parent 646d16f83d
commit 3bb096057e
64 changed files with 929 additions and 88 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT",
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT",
@@ -28,16 +28,16 @@
"wrnexus": "./dist/index.js"
},
"dependencies": {
"@wrnexus/core": "^0.2.51",
"@wrnexus/router": "^0.2.51",
"@wrnexus/csr": "^0.2.51",
"@wrnexus/compiler": "^0.2.51",
"@wrnexus/styles": "^0.2.51",
"@wrnexus/dev-server": "^0.2.51",
"@wrnexus/ui": "^0.2.51",
"@wrnexus/validation": "^0.2.51",
"@wrnexus/i18n": "^0.2.51",
"@wrnexus/db": "^0.2.51"
"@wrnexus/core": "^0.2.52",
"@wrnexus/router": "^0.2.52",
"@wrnexus/csr": "^0.2.52",
"@wrnexus/compiler": "^0.2.52",
"@wrnexus/styles": "^0.2.52",
"@wrnexus/dev-server": "^0.2.52",
"@wrnexus/ui": "^0.2.52",
"@wrnexus/validation": "^0.2.52",
"@wrnexus/i18n": "^0.2.52",
"@wrnexus/db": "^0.2.52"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.2.51"
"@wrnexus/core": "^0.2.52"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT",
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT",
@@ -25,18 +25,18 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.2.51",
"@wrnexus/router": "^0.2.51",
"@wrnexus/ssr": "^0.2.51",
"@wrnexus/csr": "^0.2.51",
"@wrnexus/compiler": "^0.2.51",
"@wrnexus/styles": "^0.2.51",
"@wrnexus/ui": "^0.2.51",
"@wrnexus/validation": "^0.2.51",
"@wrnexus/i18n": "^0.2.51",
"@wrnexus/db": "^0.2.51",
"@wrnexus/pubsub": "^0.2.51",
"@wrnexus/uploader": "^0.2.51"
"@wrnexus/core": "^0.2.52",
"@wrnexus/router": "^0.2.52",
"@wrnexus/ssr": "^0.2.52",
"@wrnexus/csr": "^0.2.52",
"@wrnexus/compiler": "^0.2.52",
"@wrnexus/styles": "^0.2.52",
"@wrnexus/ui": "^0.2.52",
"@wrnexus/validation": "^0.2.52",
"@wrnexus/i18n": "^0.2.52",
"@wrnexus/db": "^0.2.52",
"@wrnexus/pubsub": "^0.2.52",
"@wrnexus/uploader": "^0.2.52"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.2.51"
"@wrnexus/core": "^0.2.52"
},
"files": [
"dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.2.51"
"@wrnexus/core": "^0.2.52"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/native": "^0.2.51"
"@wrnexus/native": "^0.2.52"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT",
@@ -21,8 +21,8 @@
}
},
"dependencies": {
"@wrnexus/compiler": "^0.2.51",
"@wrnexus/core": "^0.2.51"
"@wrnexus/compiler": "^0.2.52",
"@wrnexus/core": "^0.2.52"
},
"files": [
"dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.2.51"
"@wrnexus/core": "^0.2.52"
},
"files": [
"dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/uploader": "^0.2.51"
"@wrnexus/uploader": "^0.2.52"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -65,7 +65,7 @@ body from either a named prop or the default slot.
### Additional controls & data display
Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`,
`tooltip`, and `table`.
`tooltip`, `table`, `FAQAccordion`, `AnnouncementBar`, and `BackToTop`.
The authoritative, always-current list is `uiComponentNames()` (below), which reads
the component directory at runtime.
@@ -0,0 +1,88 @@
component AnnouncementBar {
props {
badge = "New"
message = "WrNexus Organizations is now available."
description = "Build secure multi-tenant applications with teams, roles, domains, and enterprise SSO."
href = "/organizations"
actionLabel = "Explore organizations"
ariaLabel = "Announcement"
}
state visible = true
view {
<aside
role="region"
aria-label="{ariaLabel}"
class="relative z-[70] overflow-hidden border-b border-indigo-200 bg-indigo-50 text-slate-900 dark:border-indigo-500/20 dark:bg-indigo-500/10 dark:text-white"
class:hidden="!visible"
>
<!-- Decorative background -->
<div
aria-hidden="true"
class="pointer-events-none absolute inset-0 overflow-hidden"
>
<div class="absolute -left-20 -top-24 h-40 w-40 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-500/10"></div>
<div class="absolute -right-20 -bottom-24 h-40 w-40 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-500/10"></div>
</div>
<div
class="relative flex min-h-11 w-full items-center justify-between gap-4 px-4 py-2.5 sm:px-6 lg:px-10 xl:px-12"
>
<div class="flex min-w-0 flex-1 items-center justify-center gap-3">
<!-- Badge -->
<span
class="hidden shrink-0 items-center gap-1.5 rounded-full bg-indigo-600 px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.12em] text-white shadow-sm sm:inline-flex"
>
<span class="icon-[lucide--sparkles] h-3 w-3" aria-hidden="true" ></span>
{badge}
</span>
<!-- Message -->
<p
class="min-w-0 text-center text-xs font-medium leading-5 text-slate-700 dark:text-slate-300 sm:text-sm"
>
<span class="font-semibold text-slate-950 dark:text-white">
{message}
</span>
<span class="hidden md:inline">
{description}
</span>
</p>
<!-- CTA -->
<a
href="{href}"
class="group hidden shrink-0 items-center gap-1.5 text-xs font-bold text-indigo-700 transition hover:text-indigo-900 dark:text-indigo-300 dark:hover:text-indigo-200 sm:inline-flex"
>
{actionLabel}
<span class="icon-[lucide--arrow-right] h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" aria-hidden="true" ></span>
</a>
</div>
<!-- Mobile CTA -->
<a
href="{href}"
aria-label="{actionLabel}"
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-indigo-100 text-indigo-700 transition hover:bg-indigo-200 dark:bg-indigo-500/15 dark:text-indigo-300 dark:hover:bg-indigo-500/25 sm:hidden"
>
<span class="icon-[lucide--arrow-up-right] h-4 w-4" aria-hidden="true" ></span>
</a>
<!-- Close -->
<button
type="button"
@click="visible = false"
aria-label="Dismiss announcement"
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg text-slate-500 transition hover:bg-indigo-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-white"
>
<span class="icon-[lucide--x] h-4 w-4" aria-hidden="true" ></span>
</button>
</div>
</aside>
}
}
+93
View File
@@ -0,0 +1,93 @@
component BackToTop {
props {
label = "Back to top"
assistiveLabel = "Return to the top of the page"
threshold = 500
}
state visible = false
functions {
function updateBackToTopVisibility() {
visible = window.scrollY > threshold
}
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: "smooth"
})
}
}
lifecycle {
mount {
updateBackToTopVisibility()
window.addEventListener(
"scroll",
updateBackToTopVisibility,
{ passive: true }
)
}
unmount {
window.removeEventListener(
"scroll",
updateBackToTopVisibility
)
}
}
watch visible {
console.debug(
"Back-to-top visibility:",
value,
previous
)
}
view {
<div
class="group fixed bottom-6 right-5 z-[70] flex items-center gap-2 transition-all duration-300 sm:right-6 lg:bottom-8 lg:right-8"
class:pointer-events-none="!visible"
class:translate-y-5="!visible"
class:scale-90="!visible"
class:opacity-0="!visible"
class:pointer-events-auto="visible"
class:translate-y-0="visible"
class:scale-100="visible"
class:opacity-100="visible"
>
<!-- Hover label -->
<span
class="pointer-events-none hidden translate-x-2 whitespace-nowrap rounded-xl border border-slate-200 bg-white/95 px-3 py-2 text-xs font-semibold text-slate-700 opacity-0 shadow-lg shadow-slate-950/10 backdrop-blur-xl transition-all duration-200 group-hover:translate-x-0 group-hover:opacity-100 dark:border-white/10 dark:bg-slate-900/95 dark:text-slate-200 dark:shadow-black/30 sm:block"
>
{label}
</span>
<!-- Gradient border wrapper -->
<div
class="relative rounded-2xl bg-linear-to-br from-indigo-500 via-violet-500 to-cyan-500 p-px shadow-xl shadow-indigo-600/20 transition duration-300 group-hover:-translate-y-1 group-hover:shadow-2xl group-hover:shadow-indigo-600/30"
>
<!-- Glow -->
<div aria-hidden="true" class="absolute inset-1 rounded-2xl bg-indigo-500/20 opacity-0 blur-xl transition-opacity duration-300 group-hover:opacity-100" ></div>
<button
type="button"
@click="scrollToTop()"
aria-label="{label}"
title="{label}"
class="relative grid h-12 w-12 place-items-center rounded-[15px] bg-white/95 text-indigo-700 backdrop-blur-xl transition duration-300 hover:bg-indigo-600 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 active:scale-95 dark:bg-slate-900/95 dark:text-indigo-300 dark:hover:bg-indigo-500 dark:hover:text-white dark:focus-visible:ring-offset-slate-950 sm:h-13 sm:w-13"
>
<!-- Arrow -->
<span class="icon-[lucide--arrow-up] h-5 w-5 transition-transform duration-300 group-hover:-translate-y-0.5" aria-hidden="true" ></span>
<span class="sr-only">
{assistiveLabel}
</span>
</button>
</div>
</div>
}
}
+234
View File
@@ -0,0 +1,234 @@
component FAQAccordion {
props {
eyebrow = ""
title = "Frequently asked questions"
description = ""
items = []
centered = true
compact = false
allowMultiple = false
defaultOpenIndex = 0
showContact = false
contactText = "Still have questions?"
contactLabel = "Contact support"
contactHref = "/support"
}
state openIndexes = defaultOpenIndex >= 0 ? [defaultOpenIndex] : []
functions {
function isOpen(index) {
return openIndexes.includes(index)
}
function toggleItem(index) {
if (isOpen(index)) {
openIndexes = openIndexes.filter(
(itemIndex) => itemIndex !== index
)
} else if (allowMultiple) {
openIndexes = [...openIndexes, index]
} else {
openIndexes = [index]
}
}
}
view {
<section
aria-labelledby="faq-accordion-title"
class="w-full px-5 sm:px-8 lg:px-10 xl:px-12"
class:py-10="compact"
class:sm:py-12="compact"
class:py-14="!compact"
class:sm:py-16="!compact"
class:lg:py-20="!compact"
class:hidden="items.length === 0"
>
<div
class="mx-auto w-full"
class:max-w-5xl="centered"
class:max-w-7xl="!centered"
>
<div
class="gap-10"
class:flex="centered"
class:flex-col="centered"
class:grid="!centered"
class:lg:grid-cols-[minmax(260px,0.7fr)_minmax(0,1.3fr)]="!centered"
class:lg:items-start="!centered"
class:lg:gap-14="!centered"
>
<!-- Heading -->
<div
class="min-w-0"
class:mx-auto="centered"
class:max-w-3xl="centered"
class:text-center="centered"
class:max-w-xl="!centered"
class:lg:sticky="!centered"
class:lg:top-24="!centered"
>
<!-- Eyebrow -->
<p
class="text-xs font-bold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400"
class:hidden="eyebrow === ''"
>
{eyebrow}
</p>
<!-- Title -->
<h2
id="faq-accordion-title"
class="font-bold leading-tight tracking-tight text-slate-950 dark:text-white"
class:mt-3="eyebrow !== ''"
class:text-2xl="compact"
class:sm:text-3xl="compact"
class:text-3xl="!compact"
class:sm:text-4xl="!compact"
>
{title}
</h2>
<!-- Description -->
<p
class="mt-4 text-sm leading-7 text-slate-600 dark:text-slate-400"
class:hidden="description === ''"
class:sm:text-base="!compact"
class:mx-auto="centered"
class:max-w-2xl="centered"
class:max-w-xl="!centered"
>
{description}
</p>
<!-- Contact card -->
<div
class="mt-7 rounded-2xl border border-indigo-200/80 bg-linear-to-br from-indigo-50 via-white to-violet-50 p-5 shadow-sm dark:border-indigo-500/20 dark:from-indigo-500/10 dark:via-white/[0.03] dark:to-violet-500/10"
class:hidden="!showContact"
>
<div
class="flex items-center gap-4"
class:justify-center="centered"
>
<span
class="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-white text-indigo-600 shadow-sm ring-1 ring-inset ring-indigo-200 dark:bg-white/5 dark:text-indigo-300 dark:ring-indigo-500/20"
>
<span class="icon-[lucide--messages-square] h-5 w-5" aria-hidden="true" ></span>
</span>
<div
class="min-w-0"
class:text-left="centered"
>
<p class="text-sm font-semibold text-slate-950 dark:text-white">
{contactText}
</p>
{#if contactLabel !== '' && contactHref !== ''}
<a
href="{contactHref}"
class="mt-1 inline-flex items-center gap-1.5 text-sm font-bold text-indigo-600 transition-colors duration-200 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{contactLabel}
<span class="icon-[lucide--arrow-right] h-4 w-4" aria-hidden="true" ></span>
</a>
{/if}
</div>
</div>
</div>
</div>
<!-- Accordion -->
<div class="min-w-0">
<div class="space-y-3">
{#each items as item, index}
<article
class=" overflow-hidden rounded-2xl border transition-[border-color,background-color,box-shadow,transform] duration-300 ease-out {isOpen(index) ? 'border-indigo-200 bg-indigo-50/60 shadow-md shadow-indigo-950/5 dark:border-indigo-500/20 dark:bg-indigo-500/10' : 'border-slate-200 bg-white shadow-sm hover:-translate-y-0.5 hover:border-indigo-200 hover:bg-slate-50 dark:border-white/10 dark:bg-white/[0.03] dark:hover:border-indigo-500/20 dark:hover:bg-white/5' } "
>
<h3>
<button
type="button"
@click="toggleItem(index)"
aria-expanded="{isOpen(index)}"
aria-controls="faq-answer-{index}"
class="flex w-full items-center justify-between gap-5 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500"
class:px-4="compact"
class:py-4="compact"
class:px-5="!compact"
class:py-5="!compact"
class:sm:px-6="!compact"
>
<span class="flex min-w-0 items-start gap-3">
<!-- Number -->
<span
class=" mt-0.5 grid h-7 w-7 shrink-0 place-items-center rounded-lg text-xs font-bold transition-[background-color,color,transform] duration-300 ease-out {isOpen(index) ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-500 dark:bg-white/5 dark:text-slate-400' } "
>
{index + 1}
</span>
<!-- Question -->
<span
class="font-semibold leading-6 text-slate-950 dark:text-white"
class:text-sm="compact"
class:text-base="!compact"
>
{item.question}
</span>
</span>
<!-- Toggle icon -->
<span
class=" grid h-8 w-8 shrink-0 place-items-center rounded-lg transition-[background-color,color,transform] duration-300 ease-out {isOpen(index) ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300' : 'bg-slate-100 text-slate-500 dark:bg-white/5 dark:text-slate-400' } "
>
<span class=" icon-[lucide--chevron-down] h-4 w-4 transition-transform duration-300 ease-out {isOpen(index) ? 'rotate-180' : 'rotate-0'} " aria-hidden="true" ></span>
</span>
</button>
</h3>
<div
id="faq-answer-{index}"
role="region"
aria-hidden="{!isOpen(index)}"
class=" grid overflow-hidden transition-[grid-template-rows,opacity] duration-300 ease-out {isOpen(index) ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0' } "
>
<div class="min-h-0 overflow-hidden">
<div class="border-t border-indigo-200/60 dark:border-indigo-500/15">
<div
class=" text-sm leading-7 text-slate-600 transition-[transform,opacity] duration-300 ease-out dark:text-slate-400 {compact ? 'px-4 pb-4 pt-3' : 'px-5 pb-5 pt-4 sm:px-6 sm:pb-6 sm:pt-5' } {isOpen(index) ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0' } "
>
<p class="m-0">
{item.answer}
</p>
{#if item.linkLabel !== '' && item.href !== ''}
<a
href="{item.href}"
class=" group inline-flex items-center gap-2 text-sm font-bold text-indigo-600 transition-colors duration-200 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300 "
>
{item.linkLabel}
<span class=" icon-[lucide--arrow-up-right] h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 " aria-hidden="true" ></span>
</a>
{/if}
</div>
</div>
</div>
</div>
</article>
{/each}
</div>
</div>
</div>
</div>
<!-- Tailwind safelist -->
<span class=" hidden grid-rows-[0fr] grid-rows-[1fr] opacity-0 opacity-100 -translate-y-2 translate-y-0 rotate-0 rotate-180 " ></span>
</section>
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT",
@@ -23,7 +23,7 @@
"./ui.css": "./ui.css"
},
"dependencies": {
"@wrnexus/core": "^0.2.51"
"@wrnexus/core": "^0.2.52"
},
"files": [
"dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/uploader — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.2.51"
"@wrnexus/core": "^0.2.52"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -33,7 +33,7 @@ const {
} = require("../src/diagnostics");
Module._load = originalLoad;
function mockDocument(source) {
function mockDocument() {
return {
positionAt(offset) {
return offset;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+8
View File
@@ -533,6 +533,14 @@ const MIGRATIONS: Migration[] = [
// No generated application files require automatic migration.
},
},
{
version: "0.2.52",
id: "shared-marketing-components",
description: "Adds reusable FAQ, announcement, and back-to-top UI components.",
apply() {
// No generated application files require automatic migration.
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.2.51",
"version": "0.2.52",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -65,7 +65,7 @@ body from either a named prop or the default slot.
### Additional controls & data display
Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`,
`tooltip`, and `table`.
`tooltip`, `table`, `FAQAccordion`, `AnnouncementBar`, and `BackToTop`.
The authoritative, always-current list is `uiComponentNames()` (below), which reads
the component directory at runtime.
@@ -0,0 +1,88 @@
component AnnouncementBar {
props {
badge = "New"
message = "WrNexus Organizations is now available."
description = "Build secure multi-tenant applications with teams, roles, domains, and enterprise SSO."
href = "/organizations"
actionLabel = "Explore organizations"
ariaLabel = "Announcement"
}
state visible = true
view {
<aside
role="region"
aria-label="{ariaLabel}"
class="relative z-[70] overflow-hidden border-b border-indigo-200 bg-indigo-50 text-slate-900 dark:border-indigo-500/20 dark:bg-indigo-500/10 dark:text-white"
class:hidden="!visible"
>
<!-- Decorative background -->
<div
aria-hidden="true"
class="pointer-events-none absolute inset-0 overflow-hidden"
>
<div class="absolute -left-20 -top-24 h-40 w-40 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-500/10"></div>
<div class="absolute -right-20 -bottom-24 h-40 w-40 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-500/10"></div>
</div>
<div
class="relative flex min-h-11 w-full items-center justify-between gap-4 px-4 py-2.5 sm:px-6 lg:px-10 xl:px-12"
>
<div class="flex min-w-0 flex-1 items-center justify-center gap-3">
<!-- Badge -->
<span
class="hidden shrink-0 items-center gap-1.5 rounded-full bg-indigo-600 px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.12em] text-white shadow-sm sm:inline-flex"
>
<span class="icon-[lucide--sparkles] h-3 w-3" aria-hidden="true" ></span>
{badge}
</span>
<!-- Message -->
<p
class="min-w-0 text-center text-xs font-medium leading-5 text-slate-700 dark:text-slate-300 sm:text-sm"
>
<span class="font-semibold text-slate-950 dark:text-white">
{message}
</span>
<span class="hidden md:inline">
{description}
</span>
</p>
<!-- CTA -->
<a
href="{href}"
class="group hidden shrink-0 items-center gap-1.5 text-xs font-bold text-indigo-700 transition hover:text-indigo-900 dark:text-indigo-300 dark:hover:text-indigo-200 sm:inline-flex"
>
{actionLabel}
<span class="icon-[lucide--arrow-right] h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" aria-hidden="true" ></span>
</a>
</div>
<!-- Mobile CTA -->
<a
href="{href}"
aria-label="{actionLabel}"
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-indigo-100 text-indigo-700 transition hover:bg-indigo-200 dark:bg-indigo-500/15 dark:text-indigo-300 dark:hover:bg-indigo-500/25 sm:hidden"
>
<span class="icon-[lucide--arrow-up-right] h-4 w-4" aria-hidden="true" ></span>
</a>
<!-- Close -->
<button
type="button"
@click="visible = false"
aria-label="Dismiss announcement"
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg text-slate-500 transition hover:bg-indigo-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-white"
>
<span class="icon-[lucide--x] h-4 w-4" aria-hidden="true" ></span>
</button>
</div>
</aside>
}
}
+93
View File
@@ -0,0 +1,93 @@
component BackToTop {
props {
label = "Back to top"
assistiveLabel = "Return to the top of the page"
threshold = 500
}
state visible = false
functions {
function updateBackToTopVisibility() {
visible = window.scrollY > threshold
}
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: "smooth"
})
}
}
lifecycle {
mount {
updateBackToTopVisibility()
window.addEventListener(
"scroll",
updateBackToTopVisibility,
{ passive: true }
)
}
unmount {
window.removeEventListener(
"scroll",
updateBackToTopVisibility
)
}
}
watch visible {
console.debug(
"Back-to-top visibility:",
value,
previous
)
}
view {
<div
class="group fixed bottom-6 right-5 z-[70] flex items-center gap-2 transition-all duration-300 sm:right-6 lg:bottom-8 lg:right-8"
class:pointer-events-none="!visible"
class:translate-y-5="!visible"
class:scale-90="!visible"
class:opacity-0="!visible"
class:pointer-events-auto="visible"
class:translate-y-0="visible"
class:scale-100="visible"
class:opacity-100="visible"
>
<!-- Hover label -->
<span
class="pointer-events-none hidden translate-x-2 whitespace-nowrap rounded-xl border border-slate-200 bg-white/95 px-3 py-2 text-xs font-semibold text-slate-700 opacity-0 shadow-lg shadow-slate-950/10 backdrop-blur-xl transition-all duration-200 group-hover:translate-x-0 group-hover:opacity-100 dark:border-white/10 dark:bg-slate-900/95 dark:text-slate-200 dark:shadow-black/30 sm:block"
>
{label}
</span>
<!-- Gradient border wrapper -->
<div
class="relative rounded-2xl bg-linear-to-br from-indigo-500 via-violet-500 to-cyan-500 p-px shadow-xl shadow-indigo-600/20 transition duration-300 group-hover:-translate-y-1 group-hover:shadow-2xl group-hover:shadow-indigo-600/30"
>
<!-- Glow -->
<div aria-hidden="true" class="absolute inset-1 rounded-2xl bg-indigo-500/20 opacity-0 blur-xl transition-opacity duration-300 group-hover:opacity-100" ></div>
<button
type="button"
@click="scrollToTop()"
aria-label="{label}"
title="{label}"
class="relative grid h-12 w-12 place-items-center rounded-[15px] bg-white/95 text-indigo-700 backdrop-blur-xl transition duration-300 hover:bg-indigo-600 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 active:scale-95 dark:bg-slate-900/95 dark:text-indigo-300 dark:hover:bg-indigo-500 dark:hover:text-white dark:focus-visible:ring-offset-slate-950 sm:h-13 sm:w-13"
>
<!-- Arrow -->
<span class="icon-[lucide--arrow-up] h-5 w-5 transition-transform duration-300 group-hover:-translate-y-0.5" aria-hidden="true" ></span>
<span class="sr-only">
{assistiveLabel}
</span>
</button>
</div>
</div>
}
}
+234
View File
@@ -0,0 +1,234 @@
component FAQAccordion {
props {
eyebrow = ""
title = "Frequently asked questions"
description = ""
items = []
centered = true
compact = false
allowMultiple = false
defaultOpenIndex = 0
showContact = false
contactText = "Still have questions?"
contactLabel = "Contact support"
contactHref = "/support"
}
state openIndexes = defaultOpenIndex >= 0 ? [defaultOpenIndex] : []
functions {
function isOpen(index) {
return openIndexes.includes(index)
}
function toggleItem(index) {
if (isOpen(index)) {
openIndexes = openIndexes.filter(
(itemIndex) => itemIndex !== index
)
} else if (allowMultiple) {
openIndexes = [...openIndexes, index]
} else {
openIndexes = [index]
}
}
}
view {
<section
aria-labelledby="faq-accordion-title"
class="w-full px-5 sm:px-8 lg:px-10 xl:px-12"
class:py-10="compact"
class:sm:py-12="compact"
class:py-14="!compact"
class:sm:py-16="!compact"
class:lg:py-20="!compact"
class:hidden="items.length === 0"
>
<div
class="mx-auto w-full"
class:max-w-5xl="centered"
class:max-w-7xl="!centered"
>
<div
class="gap-10"
class:flex="centered"
class:flex-col="centered"
class:grid="!centered"
class:lg:grid-cols-[minmax(260px,0.7fr)_minmax(0,1.3fr)]="!centered"
class:lg:items-start="!centered"
class:lg:gap-14="!centered"
>
<!-- Heading -->
<div
class="min-w-0"
class:mx-auto="centered"
class:max-w-3xl="centered"
class:text-center="centered"
class:max-w-xl="!centered"
class:lg:sticky="!centered"
class:lg:top-24="!centered"
>
<!-- Eyebrow -->
<p
class="text-xs font-bold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400"
class:hidden="eyebrow === ''"
>
{eyebrow}
</p>
<!-- Title -->
<h2
id="faq-accordion-title"
class="font-bold leading-tight tracking-tight text-slate-950 dark:text-white"
class:mt-3="eyebrow !== ''"
class:text-2xl="compact"
class:sm:text-3xl="compact"
class:text-3xl="!compact"
class:sm:text-4xl="!compact"
>
{title}
</h2>
<!-- Description -->
<p
class="mt-4 text-sm leading-7 text-slate-600 dark:text-slate-400"
class:hidden="description === ''"
class:sm:text-base="!compact"
class:mx-auto="centered"
class:max-w-2xl="centered"
class:max-w-xl="!centered"
>
{description}
</p>
<!-- Contact card -->
<div
class="mt-7 rounded-2xl border border-indigo-200/80 bg-linear-to-br from-indigo-50 via-white to-violet-50 p-5 shadow-sm dark:border-indigo-500/20 dark:from-indigo-500/10 dark:via-white/[0.03] dark:to-violet-500/10"
class:hidden="!showContact"
>
<div
class="flex items-center gap-4"
class:justify-center="centered"
>
<span
class="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-white text-indigo-600 shadow-sm ring-1 ring-inset ring-indigo-200 dark:bg-white/5 dark:text-indigo-300 dark:ring-indigo-500/20"
>
<span class="icon-[lucide--messages-square] h-5 w-5" aria-hidden="true" ></span>
</span>
<div
class="min-w-0"
class:text-left="centered"
>
<p class="text-sm font-semibold text-slate-950 dark:text-white">
{contactText}
</p>
{#if contactLabel !== '' && contactHref !== ''}
<a
href="{contactHref}"
class="mt-1 inline-flex items-center gap-1.5 text-sm font-bold text-indigo-600 transition-colors duration-200 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{contactLabel}
<span class="icon-[lucide--arrow-right] h-4 w-4" aria-hidden="true" ></span>
</a>
{/if}
</div>
</div>
</div>
</div>
<!-- Accordion -->
<div class="min-w-0">
<div class="space-y-3">
{#each items as item, index}
<article
class=" overflow-hidden rounded-2xl border transition-[border-color,background-color,box-shadow,transform] duration-300 ease-out {isOpen(index) ? 'border-indigo-200 bg-indigo-50/60 shadow-md shadow-indigo-950/5 dark:border-indigo-500/20 dark:bg-indigo-500/10' : 'border-slate-200 bg-white shadow-sm hover:-translate-y-0.5 hover:border-indigo-200 hover:bg-slate-50 dark:border-white/10 dark:bg-white/[0.03] dark:hover:border-indigo-500/20 dark:hover:bg-white/5' } "
>
<h3>
<button
type="button"
@click="toggleItem(index)"
aria-expanded="{isOpen(index)}"
aria-controls="faq-answer-{index}"
class="flex w-full items-center justify-between gap-5 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500"
class:px-4="compact"
class:py-4="compact"
class:px-5="!compact"
class:py-5="!compact"
class:sm:px-6="!compact"
>
<span class="flex min-w-0 items-start gap-3">
<!-- Number -->
<span
class=" mt-0.5 grid h-7 w-7 shrink-0 place-items-center rounded-lg text-xs font-bold transition-[background-color,color,transform] duration-300 ease-out {isOpen(index) ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-500 dark:bg-white/5 dark:text-slate-400' } "
>
{index + 1}
</span>
<!-- Question -->
<span
class="font-semibold leading-6 text-slate-950 dark:text-white"
class:text-sm="compact"
class:text-base="!compact"
>
{item.question}
</span>
</span>
<!-- Toggle icon -->
<span
class=" grid h-8 w-8 shrink-0 place-items-center rounded-lg transition-[background-color,color,transform] duration-300 ease-out {isOpen(index) ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300' : 'bg-slate-100 text-slate-500 dark:bg-white/5 dark:text-slate-400' } "
>
<span class=" icon-[lucide--chevron-down] h-4 w-4 transition-transform duration-300 ease-out {isOpen(index) ? 'rotate-180' : 'rotate-0'} " aria-hidden="true" ></span>
</span>
</button>
</h3>
<div
id="faq-answer-{index}"
role="region"
aria-hidden="{!isOpen(index)}"
class=" grid overflow-hidden transition-[grid-template-rows,opacity] duration-300 ease-out {isOpen(index) ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0' } "
>
<div class="min-h-0 overflow-hidden">
<div class="border-t border-indigo-200/60 dark:border-indigo-500/15">
<div
class=" text-sm leading-7 text-slate-600 transition-[transform,opacity] duration-300 ease-out dark:text-slate-400 {compact ? 'px-4 pb-4 pt-3' : 'px-5 pb-5 pt-4 sm:px-6 sm:pb-6 sm:pt-5' } {isOpen(index) ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0' } "
>
<p class="m-0">
{item.answer}
</p>
{#if item.linkLabel !== '' && item.href !== ''}
<a
href="{item.href}"
class=" group inline-flex items-center gap-2 text-sm font-bold text-indigo-600 transition-colors duration-200 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300 "
>
{item.linkLabel}
<span class=" icon-[lucide--arrow-up-right] h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 " aria-hidden="true" ></span>
</a>
{/if}
</div>
</div>
</div>
</div>
</article>
{/each}
</div>
</div>
</div>
</div>
<!-- Tailwind safelist -->
<span class=" hidden grid-rows-[0fr] grid-rows-[1fr] opacity-0 opacity-100 -translate-y-2 translate-y-0 rotate-0 rotate-180 " ></span>
</section>
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+3
View File
@@ -6,6 +6,9 @@ import { uiComponentNames, uiComponentsDir, uiCss } from "../src/index.ts";
test("bundled UI assets are discoverable and readable", () => {
expect(uiComponentNames()).toContain("Button");
expect(uiComponentNames()).toContain("FAQAccordion");
expect(uiComponentNames()).toContain("AnnouncementBar");
expect(uiComponentNames()).toContain("BackToTop");
expect(readFileSync(join(uiComponentsDir(), "Button.wrn"), "utf8")).toContain("component Button");
expect(uiCss()).toContain("--wire-");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
"version": "0.2.51",
"version": "0.2.52",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const version = "0.2.51";
const version = "0.2.52";
const dependencyGroups = [
"dependencies",