release: WRNexusJS 0.8.0
This commit is contained in:
@@ -1,11 +1,27 @@
|
||||
component AccountStatus {
|
||||
props { status = "active" title = "Account status" activeMessage = "Your account is active and ready to use." pendingMessage = "Verify your contact details to activate your account." lockedMessage = "Your account is temporarily locked for security." disabledMessage = "Your account has been disabled." supportHref = "/support" color = "primary" size = "md" class = "" }
|
||||
props {
|
||||
status: string = "active"
|
||||
title: string = "Account status"
|
||||
activeMessage: string = "Your account is active and ready to use."
|
||||
pendingMessage: string = "Verify your contact details to activate your account."
|
||||
lockedMessage: string = "Your account is temporarily locked for security."
|
||||
disabledMessage: string = "Your account has been disabled."
|
||||
supportHref: string = "/support"
|
||||
color: string = "primary"
|
||||
size: string = "md"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section {...attrs} data-status='{status}' class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-center {class}'>
|
||||
<span class='mx-auto flex size-14 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)] text-[var(--wire-color-primary)] data-[status=locked]:text-[var(--wire-color-warning)] data-[status=disabled]:text-[var(--wire-color-danger)]'>{#if status == "active"}<span class="icon-[lucide--circle-check] size-7"></span>{:else if status == "pending"}<span class="icon-[lucide--clock-3] size-7"></span>{:else}<span class="icon-[lucide--shield-alert] size-7"></span>{/if}</span>
|
||||
<h1 class="mb-0 mt-4 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mx-auto mt-2 max-w-md text-sm text-[var(--wire-color-muted)]">{status == "active" ? activeMessage : status == "pending" ? pendingMessage : status == "locked" ? lockedMessage : disabledMessage}</p>
|
||||
{#if status != "active"}<a href='{supportHref}' class="mt-4 inline-flex h-10 items-center justify-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white">Contact support</a>{/if}
|
||||
</section>
|
||||
<Card {...attrs} title='{title}' color='{color}' size='{size}' align="center" class='w-full max-w-lg {class}'>
|
||||
<div class="flex flex-col items-center text-center" data-status='{status}'>
|
||||
<span class='flex size-14 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)] text-[var(--wire-color-primary)]'>
|
||||
{#if status == "active"}<span class="icon-[lucide--circle-check] size-7"></span>{:else if status == "pending"}<span class="icon-[lucide--clock-3] size-7"></span>{:else}<span class="icon-[lucide--shield-alert] size-7"></span>{/if}
|
||||
</span>
|
||||
<p class="mx-auto mt-3 max-w-md text-sm text-[var(--wire-color-muted)]">{status == "active" ? activeMessage : status == "pending" ? pendingMessage : status == "locked" ? lockedMessage : disabledMessage}</p>
|
||||
<Badge label='{status}' color='{status == "active" ? "success" : status == "pending" ? "warning" : "danger"}' variant="soft" size="sm" />
|
||||
{#if status != "active"}<Button href='{supportHref}' label="Contact support" color='{color}' size='{size}' class="mt-4" />{/if}
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
component AuthProviderButtons {
|
||||
outputs {
|
||||
select(payload: { provider: string; href: string })
|
||||
}
|
||||
|
||||
props {
|
||||
providers: unknown[] = []
|
||||
title: string = "Continue with"
|
||||
dividerLabel: string = "or"
|
||||
color: string = "primary"
|
||||
size: string = "md"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
client function choose(provider) {
|
||||
output.select({ provider: provider.id || provider.name || provider.label, href: provider.href || "" })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div {...attrs} class='space-y-3 {class}'>
|
||||
{#if title}<p class="m-0 text-sm font-semibold text-[var(--wire-color-text)]">{title}</p>{/if}
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
{#each providers as provider}
|
||||
<Button
|
||||
label='{provider.label || provider.name || "Continue"}'
|
||||
href='{provider.href || ""}'
|
||||
icon='{provider.icon || "icon-[lucide--log-in]"}'
|
||||
variant='{provider.variant || "outline"}'
|
||||
color='{provider.color || color}'
|
||||
size='{size}'
|
||||
fullWidth="true"
|
||||
@click='choose(provider)'
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{#if dividerLabel}
|
||||
<div class="flex items-center gap-3 text-xs text-[var(--wire-color-muted)]"><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span><span>{dividerLabel}</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
|
||||
{/if}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
component AuthSecurityNotice {
|
||||
props {
|
||||
title: string = "Security notice"
|
||||
description: string = "Your session and credentials are protected by WRNexusJS security controls."
|
||||
color: string = "info"
|
||||
size: string = "sm"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<Alert
|
||||
{...attrs}
|
||||
title='{title}'
|
||||
description='{description}'
|
||||
icon="icon-[lucide--shield-check]"
|
||||
color='{color}'
|
||||
size='{size}'
|
||||
variant="soft"
|
||||
class='text-[var(--wire-color-text)] {class}'
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
component AuthShell {
|
||||
props {
|
||||
title: string = "Welcome"
|
||||
description: string = ""
|
||||
eyebrow: string = ""
|
||||
icon: string = "icon-[lucide--shield-check]"
|
||||
footer: string = ""
|
||||
color: string = "primary"
|
||||
size: string = "md"
|
||||
maxWidth: string = "md"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<Card
|
||||
{...attrs}
|
||||
title='{title}'
|
||||
description='{description}'
|
||||
header='{eyebrow}'
|
||||
footer='{footer}'
|
||||
color='{color}'
|
||||
size='{size}'
|
||||
class='w-full {class}'
|
||||
class:max-w-sm='maxWidth === "sm"'
|
||||
class:max-w-md='maxWidth === "md"'
|
||||
class:max-w-lg='maxWidth === "lg"'
|
||||
class:max-w-xl='maxWidth === "xl"'
|
||||
class:max-w-2xl='maxWidth === "2xl"'
|
||||
class:max-w-none='maxWidth === "full"'
|
||||
>
|
||||
<div class="mb-5 flex size-12 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wire-color-primary)_12%,transparent)] text-[var(--wire-color-primary)]">
|
||||
<span class='{icon}' aria-hidden="true"></span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,39 @@
|
||||
component DeviceSessions {
|
||||
props {
|
||||
sessions = []
|
||||
currentSessionId = ""
|
||||
title = "Active sessions"
|
||||
description = "Review devices signed in to your account."
|
||||
revokeAction = "/api/auth/sessions/revoke"
|
||||
revokeSchema = "auth-session-revoke"
|
||||
revokeLabel = "Sign out"
|
||||
successMessage = "Session revoked."
|
||||
color = "primary"
|
||||
size = "md"
|
||||
class = ""
|
||||
sessions: unknown[] = []
|
||||
currentSessionId: string = ""
|
||||
title: string = "Active sessions"
|
||||
description: string = "Review devices signed in to your account."
|
||||
revokeAction: string = "/api/auth/sessions/revoke"
|
||||
revokeSchema: string = "auth-session-revoke"
|
||||
revokeLabel: string = "Sign out"
|
||||
successMessage: string = "Session revoked."
|
||||
color: string = "primary"
|
||||
size: string = "md"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-2xl rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] {class}'>
|
||||
<h2 class="m-0 text-xl font-semibold">{title}</h2>
|
||||
<p class="mt-1 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<div class="mt-5 divide-y divide-[var(--wire-color-border)]">
|
||||
<Card {...attrs} title='{title}' description='{description}' color='{color}' size='{size}' class='w-full max-w-2xl {class}'>
|
||||
<div class="divide-y divide-[var(--wire-color-border)]">
|
||||
{#each sessions as session}
|
||||
<article class="flex items-center gap-3 py-4">
|
||||
<span class="flex size-10 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)]"><span class="icon-[lucide--monitor-smartphone] size-5"></span></span>
|
||||
<Avatar fallback='{session.userAgent || "Device"}' icon="icon-[lucide--monitor-smartphone]" size="sm" color='{color}' />
|
||||
<div class="min-w-0 flex-1"><p class="m-0 truncate text-sm font-semibold">{session.userAgent || "Unknown device"}</p><p class="m-0 mt-1 text-xs text-[var(--wire-color-muted)]">{session.ip || "Unknown IP"} · Last active {session.lastSeenAt}</p></div>
|
||||
{#if session.id == currentSessionId}
|
||||
<span class="rounded-full bg-[color-mix(in_srgb,var(--wire-color-success)_12%,transparent)] px-2 py-1 text-xs font-semibold text-[var(--wire-color-success)]">Current</span>
|
||||
<Badge label="Current" color="success" variant="soft" size="sm" />
|
||||
{:else}
|
||||
<form method="post" action='{revokeAction}' data-schema='{revokeSchema}' novalidate>
|
||||
<input type="hidden" name="sessionId" value='{session.id}' />
|
||||
<p data-error="sessionId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="text-sm font-semibold text-[var(--wire-color-danger)] disabled:opacity-60">{revokeLabel}</button>
|
||||
<Button type="submit" label='{revokeLabel}' color="danger" variant="ghost" size="sm" />
|
||||
</form>
|
||||
{/if}
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
component ImpersonationBanner {
|
||||
props {
|
||||
visible = true
|
||||
targetName = "this user"
|
||||
stopAction = "/api/auth/impersonation/stop"
|
||||
stopSchema = "auth-empty"
|
||||
redirect = "/account"
|
||||
message = "You are viewing the application as"
|
||||
stopLabel = "Stop impersonating"
|
||||
color = "warning"
|
||||
size = "md"
|
||||
class = ""
|
||||
visible: boolean = true
|
||||
targetName: string = "this user"
|
||||
stopAction: string = "/api/auth/impersonation/stop"
|
||||
stopSchema: string = "auth-empty"
|
||||
redirect: string = "/account"
|
||||
message: string = "You are viewing the application as"
|
||||
stopLabel: string = "Stop impersonating"
|
||||
color: string = "warning"
|
||||
size: string = "md"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
{#if visible}
|
||||
<aside {...attrs} data-wrnexus-runtime="auth" role="status" class='flex w-full flex-wrap items-center justify-between gap-3 border-b border-[var(--wire-color-warning)] bg-[color-mix(in_srgb,var(--wire-color-warning)_14%,var(--wire-color-surface))] px-4 py-2 text-sm text-[var(--wire-color-text)] {class}'>
|
||||
<span class="flex items-center gap-2"><span class="icon-[lucide--scan-face] size-4 text-[var(--wire-color-warning)]"></span><span>{message} <strong>{targetName}</strong>.</span></span>
|
||||
<form method="post" action='{stopAction}' data-schema='{stopSchema}' data-redirect='{redirect}' novalidate><p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p><button type="submit" class="inline-flex h-8 items-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-warning)] px-3 text-xs font-semibold text-black disabled:opacity-60">{stopLabel}</button></form>
|
||||
</aside>
|
||||
<Alert {...attrs} title='{message + " " + targetName}' icon="icon-[lucide--scan-face]" color='{color}' size='{size}' variant="soft" class='{class}'>
|
||||
<form method="post" action='{stopAction}' data-schema='{stopSchema}' data-redirect='{redirect}' novalidate class="mt-2">
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<Button type="submit" label='{stopLabel}' color='{color}' size="sm" />
|
||||
</form>
|
||||
</Alert>
|
||||
{/if}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
component PasskeyButton {
|
||||
props {
|
||||
mode = "authenticate"
|
||||
label = "Continue with a passkey"
|
||||
registerLabel = "Add a passkey"
|
||||
identifier = ""
|
||||
rpId = ""
|
||||
rpName = "WRNexusJS"
|
||||
passkeyName = "Passkey"
|
||||
optionsEndpoint = ""
|
||||
verifyEndpoint = ""
|
||||
redirect = ""
|
||||
mfaHref = "/two-factor"
|
||||
conditional = false
|
||||
fullWidth = false
|
||||
loadingMessage = "Waiting for your passkey…"
|
||||
successMessage = "Passkey verified."
|
||||
color = "primary"
|
||||
size = "md"
|
||||
class = ""
|
||||
@event passkeyRegistered = function
|
||||
@event passkeyAuthenticated = function
|
||||
@event error = function
|
||||
outputs {
|
||||
passkeyRegistered(payload: { credentialId: string; response?: object })
|
||||
passkeyAuthenticated(payload: { userId?: string; sessionId?: string; response?: object })
|
||||
error(payload: { code: string; message: string })
|
||||
}
|
||||
|
||||
props {
|
||||
mode: string = "authenticate"
|
||||
label: string = "Continue with a passkey"
|
||||
registerLabel: string = "Add a passkey"
|
||||
identifier: string = ""
|
||||
rpId: string = ""
|
||||
rpName: string = "WRNexusJS"
|
||||
passkeyName: string = "Passkey"
|
||||
optionsEndpoint: string = ""
|
||||
verifyEndpoint: string = ""
|
||||
redirect: string = ""
|
||||
mfaHref: string = "/two-factor"
|
||||
conditional: boolean = false
|
||||
fullWidth: boolean = false
|
||||
loadingMessage: string = "Waiting for your passkey…"
|
||||
successMessage: string = "Passkey verified."
|
||||
color: string = "primary"
|
||||
size: string = "md"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<div {...attrs} data-wrnexus-runtime="auth" data-auth-passkey='{mode}' data-identifier='{identifier}' data-rp-id='{rpId}' data-rp-name='{rpName}' data-passkey-name='{passkeyName}' data-options-endpoint='{optionsEndpoint}' data-verify-endpoint='{verifyEndpoint}' data-redirect='{redirect}' data-mfa-href='{mfaHref}' data-conditional='{conditional}' data-loading-message='{loadingMessage}' data-success-message='{successMessage}' data-auth-state="idle" aria-busy="false" class='space-y-2 {class}'>
|
||||
<button type="button" class='inline-flex h-11 items-center justify-center gap-2 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] px-4 text-sm font-semibold text-[var(--wire-color-text)] hover:bg-[var(--wire-color-surface-2)] disabled:cursor-wait disabled:opacity-60 {fullWidth ? "w-full" : ""}'>
|
||||
<span aria-hidden="true" class="icon-[lucide--key-round] size-4"></span>
|
||||
{mode == "register" ? registerLabel : label}
|
||||
</button>
|
||||
<Button
|
||||
type="button"
|
||||
label='{mode == "register" ? registerLabel : label}'
|
||||
icon="icon-[lucide--key-round]"
|
||||
variant="outline"
|
||||
color='{color}'
|
||||
size='{size}'
|
||||
fullWidth='{fullWidth}'
|
||||
/>
|
||||
<p data-auth-status hidden role="status" class="m-0 text-xs text-[var(--wire-color-muted)]"></p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,86 +1,35 @@
|
||||
component RecoveryCodes {
|
||||
props {
|
||||
codes = []
|
||||
schema = "auth-recovery-codes"
|
||||
title = "Recovery codes"
|
||||
description = "Store these codes somewhere safe. Each code can be used once."
|
||||
downloadLabel = "Download codes"
|
||||
regenerateLabel = "Generate new codes"
|
||||
regenerateAction = "/api/auth/recovery-codes"
|
||||
successMessage = "New recovery codes generated. Previous unused codes are no longer valid."
|
||||
count = 10
|
||||
color = "primary"
|
||||
size = "md"
|
||||
class = ""
|
||||
codes: unknown[] = []
|
||||
schema: string = "auth-recovery-codes"
|
||||
title: string = "Recovery codes"
|
||||
description: string = "Store these codes somewhere safe. Each code can be used once."
|
||||
downloadLabel: string = "Download codes"
|
||||
regenerateLabel: string = "Generate new codes"
|
||||
regenerateAction: string = "/api/auth/recovery-codes"
|
||||
successMessage: string = "New recovery codes generated. Previous unused codes are no longer valid."
|
||||
count: number = 10
|
||||
color: string = "primary"
|
||||
size: string = "md"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
data-wrnexus-runtime="auth"
|
||||
data-auth-recovery-codes
|
||||
data-recovery-filename="wrnexus-recovery-codes.txt"
|
||||
class='w-full max-w-xl rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 {class}'
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-xl font-semibold">{title}</h2>
|
||||
<p class="mt-1 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
</div>
|
||||
<span class="icon-[lucide--shield-keyhole] size-6 text-[var(--wire-color-primary)]"></span>
|
||||
<Card {...attrs} title='{title}' description='{description}' color='{color}' size='{size}' class='w-full max-w-xl {class}' data-wrnexus-runtime="auth" data-auth-recovery-codes data-recovery-filename="wrnexus-recovery-codes.txt">
|
||||
<Alert title="Keep these codes private" description="Anyone with a recovery code may be able to access your account." icon="icon-[lucide--shield-alert]" color="warning" variant="soft" size="sm" />
|
||||
<div data-recovery-code-list class="mt-4 grid grid-cols-2 gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-4 font-mono text-sm sm:grid-cols-3">
|
||||
{#each codes as code}<code data-recovery-code class="rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center">{code}</code>{/each}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-recovery-code-list
|
||||
class="mt-5 grid grid-cols-2 gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-4 font-mono text-sm sm:grid-cols-3"
|
||||
>
|
||||
{#each codes as code}
|
||||
<code
|
||||
data-recovery-code
|
||||
class="rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center"
|
||||
>{code}</code>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
data-recovery-download
|
||||
class="h-10 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-4 text-sm font-semibold"
|
||||
>
|
||||
{downloadLabel}
|
||||
</button>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action='{regenerateAction}'
|
||||
data-schema='{schema}'
|
||||
novalidate
|
||||
class="flex-1"
|
||||
>
|
||||
<Button type="button" label='{downloadLabel}' icon="icon-[lucide--download]" variant="outline" color='{color}' size='{size}' data-recovery-download />
|
||||
<form method="post" action='{regenerateAction}' data-schema='{schema}' novalidate class="min-w-48 flex-1">
|
||||
<input type="hidden" name="count" value='{count}' />
|
||||
<p data-error="count" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p
|
||||
data-error="_form"
|
||||
role="alert"
|
||||
class="m-0 hidden text-xs text-[var(--wire-color-danger)]"
|
||||
></p>
|
||||
<p
|
||||
data-success='{successMessage}'
|
||||
role="status"
|
||||
hidden
|
||||
class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-xs text-[var(--wire-color-success)]"
|
||||
>
|
||||
{successMessage}
|
||||
</p>
|
||||
<button
|
||||
type="submit"
|
||||
class="h-10 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-4 text-sm font-semibold disabled:cursor-wait disabled:opacity-60"
|
||||
>
|
||||
{regenerateLabel}
|
||||
</button>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label='{regenerateLabel}' icon="icon-[lucide--refresh-cw]" variant="outline" color='{color}' size='{size}' fullWidth="true" />
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user