refactor: migrate legacy wire namespace to wrn
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("AI platform", () => {
|
||||
{ role: "user", content: "c" },
|
||||
]);
|
||||
expect(await conversations.load("one")).toHaveLength(2);
|
||||
expect(promptTemplate("Hello {{name}}")({ name: "Wire" })).toBe("Hello Wire");
|
||||
expect(promptTemplate("Hello {{name}}")({ name: "WrNexus" })).toBe("Hello WrNexus");
|
||||
const provider = guardedProvider(
|
||||
{ name: "test", generate: async () => ({ value: "ok", provider: "test" }) },
|
||||
[maxPromptLength(3)],
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
}
|
||||
|
||||
function csrfHeaders() {
|
||||
const match = document.cookie.match(/(?:^|;\s*)wire-csrf=([^;]+)/);
|
||||
const match = document.cookie.match(/(?:^|;\s*)wrn-csrf=([^;]+)/);
|
||||
return match ? { "x-csrf-token": decodeURIComponent(match[1]) } : {};
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
for (const code of codes) {
|
||||
const item = document.createElement("code");
|
||||
item.dataset.recoveryCode = "";
|
||||
item.className = "rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center";
|
||||
item.className = "rounded bg-[var(--wrn-color-surface)] px-2 py-1.5 text-center";
|
||||
item.textContent = String(code);
|
||||
list.appendChild(item);
|
||||
}
|
||||
@@ -321,10 +321,10 @@
|
||||
}
|
||||
};
|
||||
download?.addEventListener("click", onDownload);
|
||||
element.addEventListener("wire:success", onSuccess);
|
||||
element.addEventListener("wrn:success", onSuccess);
|
||||
recoveryCleanups.set(element, () => {
|
||||
download?.removeEventListener("click", onDownload);
|
||||
element.removeEventListener("wire:success", onSuccess);
|
||||
element.removeEventListener("wrn:success", onSuccess);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -356,12 +356,12 @@
|
||||
toggle(requestForm, true);
|
||||
requestForm?.querySelector("[name=identifier]")?.focus();
|
||||
};
|
||||
element.addEventListener("wire:success", onSuccess);
|
||||
element.addEventListener("wire:error", onError);
|
||||
element.addEventListener("wrn:success", onSuccess);
|
||||
element.addEventListener("wrn:error", onError);
|
||||
back?.addEventListener("click", onBack);
|
||||
flowCleanups.set(element, () => {
|
||||
element.removeEventListener("wire:success", onSuccess);
|
||||
element.removeEventListener("wire:error", onError);
|
||||
element.removeEventListener("wrn:success", onSuccess);
|
||||
element.removeEventListener("wrn:error", onError);
|
||||
back?.removeEventListener("click", onBack);
|
||||
});
|
||||
}
|
||||
@@ -387,8 +387,8 @@
|
||||
dispatch(element, "auth:mfa-required", detail);
|
||||
navigate(element.dataset.mfaHref || "/two-factor");
|
||||
};
|
||||
element.addEventListener("wire:error", onError);
|
||||
flowCleanups.set(element, () => element.removeEventListener("wire:error", onError));
|
||||
element.addEventListener("wrn:error", onError);
|
||||
flowCleanups.set(element, () => element.removeEventListener("wrn:error", onError));
|
||||
}
|
||||
|
||||
function readMfaState() {
|
||||
@@ -477,11 +477,11 @@
|
||||
onMethodChange();
|
||||
methodInput?.addEventListener("change", onMethodChange);
|
||||
form?.addEventListener("submit", onSubmit, true);
|
||||
element.addEventListener("wire:success", onSuccess);
|
||||
element.addEventListener("wrn:success", onSuccess);
|
||||
flowCleanups.set(element, () => {
|
||||
methodInput?.removeEventListener("change", onMethodChange);
|
||||
form?.removeEventListener("submit", onSubmit, true);
|
||||
element.removeEventListener("wire:success", onSuccess);
|
||||
element.removeEventListener("wrn:success", onSuccess);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -508,8 +508,8 @@
|
||||
dispatch(element, "auth:authenticator-enabled", event.detail);
|
||||
}
|
||||
};
|
||||
element.addEventListener("wire:success", onSuccess);
|
||||
flowCleanups.set(element, () => element.removeEventListener("wire:success", onSuccess));
|
||||
element.addEventListener("wrn:success", onSuccess);
|
||||
flowCleanups.set(element, () => element.removeEventListener("wrn:success", onSuccess));
|
||||
}
|
||||
|
||||
function mountElement(element) {
|
||||
|
||||
@@ -17,10 +17,10 @@ component AccountStatus {
|
||||
view {
|
||||
<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)]'>
|
||||
<span class='flex size-14 items-center justify-center rounded-full bg-[var(--wrn-color-surface-2)] text-[var(--wrn-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>
|
||||
<p class="mx-auto mt-3 max-w-md text-sm text-[var(--wrn-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>
|
||||
|
||||
@@ -22,7 +22,7 @@ component AuthProviderButtons {
|
||||
|
||||
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}
|
||||
{#if title}<p class="m-0 text-sm font-semibold text-[var(--wrn-color-text)]">{title}</p>{/if}
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
{#each providers as provider}
|
||||
<Button
|
||||
@@ -38,7 +38,7 @@ component AuthProviderButtons {
|
||||
{/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>
|
||||
<div class="flex items-center gap-3 text-xs text-[var(--wrn-color-muted)]"><span class="h-px flex-1 bg-[var(--wrn-color-border)]"></span><span>{dividerLabel}</span><span class="h-px flex-1 bg-[var(--wrn-color-border)]"></span></div>
|
||||
{/if}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ component AuthSecurityNotice {
|
||||
color='{color}'
|
||||
size='{size}'
|
||||
variant="soft"
|
||||
class='text-[var(--wire-color-text)] {class}'
|
||||
class='text-[var(--wrn-color-text)] {class}'
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ component AuthShell {
|
||||
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)]">
|
||||
<div class="mb-5 flex size-12 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wrn-color-primary)_12%,transparent)] text-[var(--wrn-color-primary)]">
|
||||
<span class='{icon}' aria-hidden="true"></span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
|
||||
@@ -23,11 +23,11 @@ component AuthenticatorSetup {
|
||||
{...attrs}
|
||||
data-wrnexus-runtime="auth"
|
||||
data-auth-authenticator-setup
|
||||
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-[var(--wire-color-text)] {class}'
|
||||
class='w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] {class}'
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="flex size-11 shrink-0 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-[lucide--scan-line] size-5"></span></span>
|
||||
<div><h1 class="m-0 text-xl font-semibold">{title}</h1><p class="mb-0 mt-1 text-sm text-[var(--wire-color-muted)]">{description}</p></div>
|
||||
<span class="flex size-11 shrink-0 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wrn-color-primary)_12%,transparent)] text-[var(--wrn-color-primary)]"><span class="icon-[lucide--scan-line] size-5"></span></span>
|
||||
<div><h1 class="m-0 text-xl font-semibold">{title}</h1><p class="mb-0 mt-1 text-sm text-[var(--wrn-color-muted)]">{description}</p></div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
@@ -39,22 +39,22 @@ component AuthenticatorSetup {
|
||||
class='mt-5 space-y-4 {credentialId == "" ? "" : "hidden"}'
|
||||
>
|
||||
<Input id="auth-authenticator-label" name="label" label="Authenticator name" value="Authenticator" autocomplete="off" icon="icon-[lucide--smartphone]" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<Button type="submit" label="{setupLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
|
||||
<div data-auth-authenticator-details class='{credentialId == "" ? "hidden" : ""}'>
|
||||
<div class="mt-5 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-4">
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-wide text-[var(--wire-color-muted)]">Manual setup key</p>
|
||||
<div class="mt-5 rounded-[var(--wrn-radius-sm)] bg-[var(--wrn-color-surface-2)] p-4">
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-wide text-[var(--wrn-color-muted)]">Manual setup key</p>
|
||||
<code data-auth-authenticator-secret class="mt-2 block break-all text-sm font-semibold tracking-wider">{secret}</code>
|
||||
<code data-auth-authenticator-uri class="mt-3 block max-h-24 overflow-auto break-all text-xs text-[var(--wire-color-muted)]">{uri}</code>
|
||||
<code data-auth-authenticator-uri class="mt-3 block max-h-24 overflow-auto break-all text-xs text-[var(--wrn-color-muted)]">{uri}</code>
|
||||
</div>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-auth-authenticator-confirm novalidate class="mt-5 space-y-4">
|
||||
<input type="hidden" name="credentialId" value='{credentialId}' />
|
||||
<PinInput label="Authenticator code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-error="credentialId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="credentialId" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -17,19 +17,19 @@ component DeviceSessions {
|
||||
|
||||
view {
|
||||
<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)]">
|
||||
<div class="divide-y divide-[var(--wrn-color-border)]">
|
||||
{#each sessions as session}
|
||||
<article class="flex items-center gap-3 py-4">
|
||||
<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>
|
||||
<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(--wrn-color-muted)]">{session.ip || "Unknown IP"} · Last active {session.lastSeenAt}</p></div>
|
||||
{#if session.id == currentSessionId}
|
||||
<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>
|
||||
<p data-error="sessionId" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label='{revokeLabel}' color="danger" variant="ghost" size="sm" />
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
@@ -16,17 +16,17 @@ component ForgotPassword {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<Input id="auth-recovery-identifier" name="identifier" label="{identifierLabel}" autocomplete="username" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||
<slot></slot>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Back to sign in</a>
|
||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm text-[var(--wrn-color-primary)] hover:underline">Back to sign in</a>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ component ImpersonationBanner {
|
||||
{#if visible}
|
||||
<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>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<Button type="submit" label='{stopLabel}' color='{color}' size="sm" />
|
||||
</form>
|
||||
</Alert>
|
||||
|
||||
@@ -17,18 +17,18 @@ component InvitationAccept {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<span class="flex size-11 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-[lucide--user-round-plus] size-5"></span></span>
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||
<span class="flex size-11 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wrn-color-primary)_12%,transparent)] text-[var(--wrn-color-primary)]"><span class="icon-[lucide--user-round-plus] size-5"></span></span>
|
||||
<h1 class="mb-0 mt-4 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mb-0 mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<div class="mt-5 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] p-4 text-sm"><p class="m-0 font-semibold">{organization}</p><p class="mb-0 mt-1 text-[var(--wire-color-muted)]">Invited by {inviter}</p></div>
|
||||
<p class="mb-0 mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
<div class="mt-5 rounded-[var(--wrn-radius-sm)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface-2)] p-4 text-sm"><p class="m-0 font-semibold">{organization}</p><p class="mb-0 mt-1 text-[var(--wrn-color-muted)]">Invited by {inviter}</p></div>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-5 space-y-4">
|
||||
<input type="hidden" name="token" value='{token}' />
|
||||
<Input id="auth-invitation-name" name="displayName" label="Display name" autocomplete="name" placeholder="Enter your display name" icon="icon-[lucide--user]" color="{color}" size="{size}" />
|
||||
<StrongPassword label="Create password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
|
||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -15,17 +15,17 @@ component MagicLinkSignIn {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<div class="mb-5 flex size-11 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-[lucide--mail-check] size-5"></span></div>
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||
<div class="mb-5 flex size-11 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wrn-color-primary)_12%,transparent)] text-[var(--wrn-color-primary)]"><span class="icon-[lucide--mail-check] size-5"></span></div>
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mb-0 mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="mb-0 mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<Input id="auth-magic-link-identifier" name="identifier" type="email" label="{emailLabel}" autocomplete="email" placeholder="Enter your email address" icon="icon-[lucide--mail]" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm font-medium text-[var(--wire-color-primary)] hover:underline">Return to password sign-in</a>
|
||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm font-medium text-[var(--wrn-color-primary)] hover:underline">Return to password sign-in</a>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ component OtpSignIn {
|
||||
data-wrnexus-runtime="auth"
|
||||
data-auth-otp-sign-in
|
||||
data-mfa-href='{mfaHref}'
|
||||
class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'
|
||||
class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
|
||||
>
|
||||
<div class="mb-5 flex size-11 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wire-color-primary)_12%,transparent)] text-[var(--wire-color-primary)]">
|
||||
<div class="mb-5 flex size-11 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wrn-color-primary)_12%,transparent)] text-[var(--wrn-color-primary)]">
|
||||
<span class="icon-[lucide--key-round] size-5"></span>
|
||||
</div>
|
||||
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
|
||||
<p class="mb-0 mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="mb-0 mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
@@ -44,9 +44,9 @@ component OtpSignIn {
|
||||
>
|
||||
<input type="hidden" name="method" value='{method}' />
|
||||
<Input id="auth-otp-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username" placeholder="Enter your email address or phone number" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{requestLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
|
||||
@@ -61,8 +61,8 @@ component OtpSignIn {
|
||||
<input type="hidden" name="challengeId" value='{challengeId}' />
|
||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||
<PinInput label="One-time code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<Button type="submit" label="{verifyLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
<Button type="button" data-auth-otp-back label="Use a different account" variant="ghost" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
|
||||
@@ -39,7 +39,7 @@ component PasskeyButton {
|
||||
size='{size}'
|
||||
fullWidth='{fullWidth}'
|
||||
/>
|
||||
<p data-auth-status hidden role="status" class="m-0 text-xs text-[var(--wire-color-muted)]"></p>
|
||||
<p data-auth-status hidden role="status" class="m-0 text-xs text-[var(--wrn-color-muted)]"></p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,16 @@ component RecoveryCodes {
|
||||
view {
|
||||
<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 data-recovery-code-list class="mt-4 grid grid-cols-2 gap-2 rounded-[var(--wrn-radius-sm)] bg-[var(--wrn-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(--wrn-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" 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 text-xs text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="count" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label='{regenerateLabel}' icon="icon-[lucide--refresh-cw]" variant="outline" color='{color}' size='{size}' fullWidth="true" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -15,15 +15,15 @@ component ResetPassword {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-6 space-y-4">
|
||||
<input type="hidden" name="token" value='{token}' />
|
||||
<StrongPassword label="New password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
|
||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -25,10 +25,10 @@ component SignIn {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||
<header class="mb-6 space-y-1.5">
|
||||
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
|
||||
<p class="m-0 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
</header>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="space-y-4">
|
||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||
@@ -52,15 +52,15 @@ component SignIn {
|
||||
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
|
||||
{/if}
|
||||
<slot></slot>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
{#if showPasskey}
|
||||
<div class="my-4 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>or</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
|
||||
<div class="my-4 flex items-center gap-3 text-xs text-[var(--wrn-color-muted)]"><span class="h-px flex-1 bg-[var(--wrn-color-border)]"></span><span>or</span><span class="h-px flex-1 bg-[var(--wrn-color-border)]"></span></div>
|
||||
<PasskeyButton mode="authenticate" identifier="" mfaHref='{mfaHref}' fullWidth="true" />
|
||||
{/if}
|
||||
{#if showSignUp}
|
||||
<p class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]">New here? <a href='{signUpHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Create an account</a></p>
|
||||
<p class="mb-0 mt-5 text-center text-sm text-[var(--wrn-color-muted)]">New here? <a href='{signUpHref}' class="font-semibold text-[var(--wrn-color-primary)] hover:underline">Create an account</a></p>
|
||||
{/if}
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ component SignUp {
|
||||
<section
|
||||
{...attrs}
|
||||
data-wrnexus-runtime="auth"
|
||||
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-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'
|
||||
class='w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
|
||||
>
|
||||
<header
|
||||
class="mb-6 space-y-1.5"
|
||||
@@ -38,7 +38,7 @@ component SignUp {
|
||||
{title}
|
||||
</h1>
|
||||
<p
|
||||
class="m-0 text-sm text-[var(--wire-color-muted)]"
|
||||
class="m-0 text-sm text-[var(--wrn-color-muted)]"
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
@@ -105,7 +105,7 @@ component SignUp {
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-auth-sign-up__wide space-y-1.5 sm:col-span-2"
|
||||
class="wrn-auth-sign-up__wide space-y-1.5 sm:col-span-2"
|
||||
>
|
||||
<StrongPassword
|
||||
label="Password"
|
||||
@@ -130,7 +130,7 @@ component SignUp {
|
||||
label="I agree to the"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
class="wire-auth-sign-up__wide sm:col-span-2"
|
||||
class="wrn-auth-sign-up__wide sm:col-span-2"
|
||||
>
|
||||
<span
|
||||
class="flex items-center"
|
||||
@@ -157,7 +157,7 @@ component SignUp {
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-auth-sign-up__wide sm:col-span-2"
|
||||
class="wrn-auth-sign-up__wide sm:col-span-2"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -165,14 +165,14 @@ component SignUp {
|
||||
<p
|
||||
data-error="_form"
|
||||
role="alert"
|
||||
class="wire-auth-sign-up__wide m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)] sm:col-span-2"
|
||||
class="wrn-auth-sign-up__wide m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)] sm:col-span-2"
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
data-success="Account created successfully."
|
||||
role="status"
|
||||
hidden
|
||||
class="wire-auth-sign-up__wide m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)] sm:col-span-2"
|
||||
class="wrn-auth-sign-up__wide m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)] sm:col-span-2"
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -183,20 +183,20 @@ component SignUp {
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
fullWidth="true"
|
||||
class="wire-auth-sign-up__wide sm:col-span-2"
|
||||
class="wrn-auth-sign-up__wide sm:col-span-2"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<p
|
||||
class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]"
|
||||
class="mb-0 mt-5 text-center text-sm text-[var(--wrn-color-muted)]"
|
||||
>
|
||||
Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Sign in</a>
|
||||
Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wrn-color-primary)] hover:underline">Sign in</a>
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-auth-sign-up__wide {
|
||||
.wrn-auth-sign-up__wide {
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -30,24 +30,24 @@ component TwoFactorChallenge {
|
||||
data-auth-mfa-challenge
|
||||
data-mfa-otp-action='{otpAction}'
|
||||
data-mfa-initial-method='{method}'
|
||||
class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'
|
||||
class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
|
||||
>
|
||||
<div class="mb-5 flex size-11 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-[lucide--shield-check] size-5"></span></div>
|
||||
<div class="mb-5 flex size-11 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wrn-color-primary)_12%,transparent)] text-[var(--wrn-color-primary)]"><span class="icon-[lucide--shield-check] size-5"></span></div>
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-auth-mfa-form novalidate class="mt-6 space-y-4">
|
||||
<input type="hidden" name="mfaToken" value='{mfaToken}' />
|
||||
<input type="hidden" name="challengeId" value='{challengeId}' />
|
||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||
<Select id="auth-mfa-method" name="method" data-auth-mfa-method label="Verification method" value="{method}" options="{methodOptions}" color="{color}" size="{size}" />
|
||||
<PinInput label="Verification code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-auth-mfa-status role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-primary)_10%,transparent)] p-3 text-sm text-[var(--wire-color-primary)]"></p>
|
||||
<p data-error="mfaToken" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-auth-mfa-status role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-primary)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-primary)]"></p>
|
||||
<p data-error="mfaToken" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<a href='{recoveryHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Account recovery options</a>
|
||||
<a href='{recoveryHref}' class="mt-4 inline-flex text-sm text-[var(--wrn-color-primary)] hover:underline">Account recovery options</a>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,20 @@ component VerifyEmail {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<Input id="auth-email-token" name="token" label="Verification token" value="{token}" autocomplete="one-time-code" placeholder="Enter your verification token" icon="icon-[lucide--badge-check]" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
||||
<input type="hidden" name="type" value='{kind}' />
|
||||
<input type="hidden" name="identifier" value='{identifier}' />
|
||||
<p data-error="type" 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-error="type" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -18,20 +18,20 @@ component VerifyPhone {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<p class="mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<PinInput label="Verification token" name="token" value="{token}" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm 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-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-success)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-success)]">{successMessage}</p>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
||||
<input type="hidden" name="type" value='{kind}' />
|
||||
<input type="hidden" name="identifier" value='{identifier}' />
|
||||
<p data-error="type" 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-error="type" class="m-0 min-h-4 text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wrn-color-danger)]"></p>
|
||||
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/auth",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.11",
|
||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
+10
-10
@@ -213,33 +213,33 @@ function authClientSource(schemas: AuthSchemaSet): string {
|
||||
(function () {
|
||||
var defaults = ${descriptors};
|
||||
|
||||
window.__wireSchemas =
|
||||
window.__wireSchemas || {};
|
||||
window.__wrnSchemas =
|
||||
window.__wrnSchemas || {};
|
||||
|
||||
Object.keys(defaults).forEach(
|
||||
function (name) {
|
||||
if (!(name in window.__wireSchemas)) {
|
||||
window.__wireSchemas[name] =
|
||||
if (!(name in window.__wrnSchemas)) {
|
||||
window.__wrnSchemas[name] =
|
||||
defaults[name];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
window.__wireValidate &&
|
||||
typeof window.__wireValidate
|
||||
window.__wrnValidate &&
|
||||
typeof window.__wrnValidate
|
||||
.registerSchemas === "function"
|
||||
) {
|
||||
window.__wireValidate.registerSchemas(
|
||||
window.__wrnValidate.registerSchemas(
|
||||
defaults,
|
||||
document
|
||||
);
|
||||
} else if (
|
||||
window.__wireValidate &&
|
||||
typeof window.__wireValidate.init ===
|
||||
window.__wrnValidate &&
|
||||
typeof window.__wrnValidate.init ===
|
||||
"function"
|
||||
) {
|
||||
window.__wireValidate.init(document);
|
||||
window.__wrnValidate.init(document);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ test("all authentication components use theme tokens and Tailwind utilities", ()
|
||||
for (const file of files) {
|
||||
const source = readFileSync(join(directory, file), "utf8");
|
||||
expect(source).toContain("component ");
|
||||
expect(source).toContain("--wire-");
|
||||
expect(source).toContain("--wrn-");
|
||||
expect(source).not.toContain("<style");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ function context(request: Request): Context {
|
||||
ip: "127.0.0.1",
|
||||
user: null,
|
||||
cookies: {
|
||||
get: (name: string) => (name === "wire-csrf" ? "route-csrf-token" : undefined),
|
||||
get: (name: string) => (name === "wrn-csrf" ? "route-csrf-token" : undefined),
|
||||
} as Context["cookies"],
|
||||
localStorage: {} as Context["localStorage"],
|
||||
session: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("end-to-end authorization", () => {
|
||||
expect(await can(guest, "post:write")).toBe(false);
|
||||
|
||||
// Only denials were audited, and only alice's requests used the resolver
|
||||
// that was wired to this audit sink.
|
||||
// that was connected to this audit sink.
|
||||
expect(audit.events.length).toBeGreaterThan(0);
|
||||
expect(audit.events.every((event) => !event.allowed)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ export const HEAD = POST;
|
||||
/>
|
||||
```
|
||||
|
||||
The component uses Tailwind utilities and `--wire-*` theme variables. It has no companion component CSS file.
|
||||
The component uses Tailwind utilities and `--wrn-*` theme variables. It has no companion component CSS file.
|
||||
|
||||
### Main props
|
||||
|
||||
|
||||
@@ -886,7 +886,7 @@
|
||||
|
||||
function onFormSubmit(state, event) {
|
||||
// Schema-backed forms run this guard after their ordinary fields validate.
|
||||
if (state.form && state.form.__wireValidateBound) return;
|
||||
if (state.form && state.form.__wrnValidateBound) return;
|
||||
if (passesSubmitGuard(state)) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
@@ -978,8 +978,8 @@
|
||||
state.submitGuard = function () {
|
||||
return passesSubmitGuard(state);
|
||||
};
|
||||
state.form.__wireSubmitGuards = state.form.__wireSubmitGuards || [];
|
||||
state.form.__wireSubmitGuards.push(state.submitGuard);
|
||||
state.form.__wrnSubmitGuards = state.form.__wrnSubmitGuards || [];
|
||||
state.form.__wrnSubmitGuards.push(state.submitGuard);
|
||||
state.formSubmitListener = function (event) {
|
||||
onFormSubmit(state, event);
|
||||
};
|
||||
@@ -1003,8 +1003,8 @@
|
||||
createChallenge(state, state.config.presentation);
|
||||
};
|
||||
state.form.addEventListener("submit", state.formSubmitListener, true);
|
||||
state.form.addEventListener("wire:success", state.formSuccessListener);
|
||||
state.form.addEventListener("wire:error", state.formErrorListener);
|
||||
state.form.addEventListener("wrn:success", state.formSuccessListener);
|
||||
state.form.addEventListener("wrn:error", state.formErrorListener);
|
||||
}
|
||||
|
||||
updateNotRobotState(state);
|
||||
@@ -1035,16 +1035,16 @@
|
||||
if (state.form && state.formSubmitListener) {
|
||||
state.form.removeEventListener("submit", state.formSubmitListener, true);
|
||||
}
|
||||
if (state.form && state.form.__wireSubmitGuards && state.submitGuard) {
|
||||
state.form.__wireSubmitGuards = state.form.__wireSubmitGuards.filter(function (guard) {
|
||||
if (state.form && state.form.__wrnSubmitGuards && state.submitGuard) {
|
||||
state.form.__wrnSubmitGuards = state.form.__wrnSubmitGuards.filter(function (guard) {
|
||||
return guard !== state.submitGuard;
|
||||
});
|
||||
}
|
||||
if (state.form && state.formSuccessListener) {
|
||||
state.form.removeEventListener("wire:success", state.formSuccessListener);
|
||||
state.form.removeEventListener("wrn:success", state.formSuccessListener);
|
||||
}
|
||||
if (state.form && state.formErrorListener) {
|
||||
state.form.removeEventListener("wire:error", state.formErrorListener);
|
||||
state.form.removeEventListener("wrn:error", state.formErrorListener);
|
||||
}
|
||||
try {
|
||||
if (state.externalApi && state.externalWidgetId != null) {
|
||||
|
||||
@@ -99,7 +99,7 @@ component Captcha {
|
||||
data-captcha-status="idle"
|
||||
data-server-verification-required="true"
|
||||
aria-busy="false"
|
||||
class='group/captcha relative flex w-full max-w-lg flex-col gap-3 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-4 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] [--captcha-accent:var(--wire-color-primary)] data-[captcha-color=secondary]:[--captcha-accent:var(--wire-color-secondary)] data-[captcha-color=success]:[--captcha-accent:var(--wire-color-success)] data-[captcha-color=warning]:[--captcha-accent:var(--wire-color-warning)] data-[captcha-color=danger]:[--captcha-accent:var(--wire-color-danger)] data-[captcha-color=info]:[--captcha-accent:var(--wire-color-info)] data-[captcha-size=compact]:max-w-xs data-[captcha-size=compact]:gap-1.5 data-[captcha-size=compact]:p-2 data-[captcha-size=big]:max-w-2xl data-[captcha-size=big]:gap-4 data-[captcha-size=big]:p-5 data-[captcha-compact=true]:max-w-xs data-[captcha-compact=true]:gap-1.5 data-[captcha-compact=true]:p-2 data-[captcha-layout=horizontal]:max-w-2xl data-[captcha-layout=horizontal]:grid data-[captcha-layout=horizontal]:grid-cols-1 sm:data-[captcha-layout=horizontal]:grid-cols-[minmax(0,1fr)_minmax(12rem,0.8fr)] sm:data-[captcha-layout=horizontal]:items-start data-[captcha-type=not-robot]:max-w-sm data-[captcha-type=not-robot]:gap-0 data-[captcha-type=not-robot]:p-0 data-[captcha-disabled=true]:pointer-events-none data-[captcha-disabled=true]:opacity-60 data-[captcha-status=verified]:border-[var(--wire-color-success)] data-[captcha-status=incorrect]:border-[var(--wire-color-danger)] data-[captcha-status=expired]:border-[var(--wire-color-danger)] data-[captcha-status=network-error]:border-[var(--wire-color-danger)] data-[captcha-status=provider-error]:border-[var(--wire-color-danger)] {class}'
|
||||
class='group/captcha relative flex w-full max-w-lg flex-col gap-3 rounded-[var(--wrn-radius-sm)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-4 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-1)] [--captcha-accent:var(--wrn-color-primary)] data-[captcha-color=secondary]:[--captcha-accent:var(--wrn-color-secondary)] data-[captcha-color=success]:[--captcha-accent:var(--wrn-color-success)] data-[captcha-color=warning]:[--captcha-accent:var(--wrn-color-warning)] data-[captcha-color=danger]:[--captcha-accent:var(--wrn-color-danger)] data-[captcha-color=info]:[--captcha-accent:var(--wrn-color-info)] data-[captcha-size=compact]:max-w-xs data-[captcha-size=compact]:gap-1.5 data-[captcha-size=compact]:p-2 data-[captcha-size=big]:max-w-2xl data-[captcha-size=big]:gap-4 data-[captcha-size=big]:p-5 data-[captcha-compact=true]:max-w-xs data-[captcha-compact=true]:gap-1.5 data-[captcha-compact=true]:p-2 data-[captcha-layout=horizontal]:max-w-2xl data-[captcha-layout=horizontal]:grid data-[captcha-layout=horizontal]:grid-cols-1 sm:data-[captcha-layout=horizontal]:grid-cols-[minmax(0,1fr)_minmax(12rem,0.8fr)] sm:data-[captcha-layout=horizontal]:items-start data-[captcha-type=not-robot]:max-w-sm data-[captcha-type=not-robot]:gap-0 data-[captcha-type=not-robot]:p-0 data-[captcha-disabled=true]:pointer-events-none data-[captcha-disabled=true]:opacity-60 data-[captcha-status=verified]:border-[var(--wrn-color-success)] data-[captcha-status=incorrect]:border-[var(--wrn-color-danger)] data-[captcha-status=expired]:border-[var(--wrn-color-danger)] data-[captcha-status=network-error]:border-[var(--wrn-color-danger)] data-[captcha-status=provider-error]:border-[var(--wrn-color-danger)] {class}'
|
||||
>
|
||||
<header data-captcha-header class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-layout=horizontal]/captcha:col-span-full group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<div class='min-w-0'>
|
||||
@@ -108,11 +108,11 @@ component Captcha {
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--shield-check] size-5 shrink-0 text-[var(--captcha-accent)] group-data-[captcha-size=compact]/captcha:size-4'
|
||||
></span>
|
||||
<h3 class='m-0 text-sm font-semibold leading-5 text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:text-base'>
|
||||
<h3 class='m-0 text-sm font-semibold leading-5 text-[var(--wrn-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:text-base'>
|
||||
{label}
|
||||
</h3>
|
||||
</div>
|
||||
<p class='m-0 mt-1 text-xs leading-5 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-size=big]/captcha:text-sm'>
|
||||
<p class='m-0 mt-1 text-xs leading-5 text-[var(--wrn-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-size=big]/captcha:text-sm'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -120,7 +120,7 @@ component Captcha {
|
||||
<span
|
||||
data-captcha-verified-badge
|
||||
hidden
|
||||
class='shrink-0 items-center gap-1 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)] group-data-[captcha-size=compact]/captcha:gap-0.5 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:py-0.5 group-data-[captcha-size=compact]/captcha:text-[10px]'
|
||||
class='shrink-0 items-center gap-1 rounded-full bg-[color-mix(in_srgb,var(--wrn-color-success)_12%,transparent)] px-2 py-1 text-xs font-semibold text-[var(--wrn-color-success)] group-data-[captcha-size=compact]/captcha:gap-0.5 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:py-0.5 group-data-[captcha-size=compact]/captcha:text-[10px]'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--circle-check] size-3.5 group-data-[captcha-size=compact]/captcha:size-3'></span>
|
||||
Verified
|
||||
@@ -130,13 +130,13 @@ component Captcha {
|
||||
<div
|
||||
data-captcha-provider-mount
|
||||
hidden
|
||||
class='flex min-h-16 w-full items-center justify-center overflow-hidden rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-2 group-data-[captcha-size=compact]/captcha:min-h-12 group-data-[captcha-size=compact]/captcha:p-1.5'
|
||||
class='flex min-h-16 w-full items-center justify-center overflow-hidden rounded-[var(--wrn-radius-sm)] bg-[var(--wrn-color-surface-2)] p-2 group-data-[captcha-size=compact]/captcha:min-h-12 group-data-[captcha-size=compact]/captcha:p-1.5'
|
||||
></div>
|
||||
|
||||
<div
|
||||
data-captcha-loading
|
||||
hidden
|
||||
class='flex min-h-28 items-center justify-center gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] text-sm text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:min-h-14 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:min-h-36 group-data-[captcha-type=not-robot]/captcha:m-3 group-data-[captcha-type=not-robot]/captcha:min-h-16'
|
||||
class='flex min-h-28 items-center justify-center gap-2 rounded-[var(--wrn-radius-sm)] bg-[var(--wrn-color-surface-2)] text-sm text-[var(--wrn-color-muted)] group-data-[captcha-size=compact]/captcha:min-h-14 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:min-h-36 group-data-[captcha-type=not-robot]/captcha:m-3 group-data-[captcha-type=not-robot]/captcha:min-h-16'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--loader-circle] size-5 animate-spin group-data-[captcha-size=compact]/captcha:size-4'></span>
|
||||
Loading challenge…
|
||||
@@ -144,8 +144,8 @@ component Captcha {
|
||||
|
||||
<div data-captcha-challenge hidden class='flex min-w-0 flex-col gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-size=big]/captcha:gap-4 group-data-[captcha-type=not-robot]/captcha:p-3'>
|
||||
<div data-captcha-prompt-row class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-2'>
|
||||
<p data-captcha-prompt class='m-0 text-sm font-medium text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4'></p>
|
||||
<span data-captcha-countdown hidden class='shrink-0 text-xs tabular-nums text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px]'></span>
|
||||
<p data-captcha-prompt class='m-0 text-sm font-medium text-[var(--wrn-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4'></p>
|
||||
<span data-captcha-countdown hidden class='shrink-0 text-xs tabular-nums text-[var(--wrn-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px]'></span>
|
||||
</div>
|
||||
|
||||
<div data-captcha-not-robot hidden class='w-full'>
|
||||
@@ -153,23 +153,23 @@ component Captcha {
|
||||
data-captcha-not-robot-button
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
class='flex min-h-20 w-full items-center gap-3 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-4 py-3 text-left transition-[border-color,box-shadow,background-color] hover:border-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-wait disabled:opacity-80 group-data-[captcha-size=compact]/captcha:min-h-16 group-data-[captcha-size=compact]/captcha:gap-2.5 group-data-[captcha-size=compact]/captcha:px-3 group-data-[captcha-size=compact]/captcha:py-2.5 group-data-[captcha-size=big]/captcha:min-h-24 group-data-[captcha-size=big]/captcha:px-5 group-data-[captcha-size=big]/captcha:py-4'
|
||||
class='flex min-h-20 w-full items-center gap-3 rounded-[var(--wrn-radius-sm)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface-2)] px-4 py-3 text-left transition-[border-color,box-shadow,background-color] hover:border-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-wait disabled:opacity-80 group-data-[captcha-size=compact]/captcha:min-h-16 group-data-[captcha-size=compact]/captcha:gap-2.5 group-data-[captcha-size=compact]/captcha:px-3 group-data-[captcha-size=compact]/captcha:py-2.5 group-data-[captcha-size=big]/captcha:min-h-24 group-data-[captcha-size=big]/captcha:px-5 group-data-[captcha-size=big]/captcha:py-4'
|
||||
>
|
||||
<span
|
||||
data-captcha-not-robot-control
|
||||
aria-hidden="true"
|
||||
class='inline-flex size-7 shrink-0 items-center justify-center rounded-md border-2 border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] text-white transition-colors data-[verified=true]:border-[var(--wire-color-success)] data-[verified=true]:bg-[var(--wire-color-success)] group-data-[captcha-size=compact]/captcha:size-6 group-data-[captcha-size=big]/captcha:size-8'
|
||||
class='inline-flex size-7 shrink-0 items-center justify-center rounded-md border-2 border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] text-white transition-colors data-[verified=true]:border-[var(--wrn-color-success)] data-[verified=true]:bg-[var(--wrn-color-success)] group-data-[captcha-size=compact]/captcha:size-6 group-data-[captcha-size=big]/captcha:size-8'
|
||||
>
|
||||
<span data-captcha-not-robot-empty class='size-full'></span>
|
||||
<span data-captcha-not-robot-spinner hidden class='icon-[lucide--loader-circle] size-4 animate-spin text-[var(--captcha-accent)]'></span>
|
||||
<span data-captcha-not-robot-check hidden class='icon-[lucide--check] size-5'></span>
|
||||
</span>
|
||||
|
||||
<span data-captcha-not-robot-label class='min-w-0 flex-1 text-sm font-semibold text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:text-base'>
|
||||
<span data-captcha-not-robot-label class='min-w-0 flex-1 text-sm font-semibold text-[var(--wrn-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:text-base'>
|
||||
I'm not a robot
|
||||
</span>
|
||||
|
||||
<span class='flex shrink-0 flex-col items-center gap-0.5 text-[10px] leading-none text-[var(--wire-color-muted)]'>
|
||||
<span class='flex shrink-0 flex-col items-center gap-0.5 text-[10px] leading-none text-[var(--wrn-color-muted)]'>
|
||||
<span aria-hidden="true" class='icon-[lucide--shield-check] size-6 text-[var(--captcha-accent)] group-data-[captcha-size=compact]/captcha:size-5 group-data-[captcha-size=big]/captcha:size-7'></span>
|
||||
<span>WRNexus</span>
|
||||
</span>
|
||||
@@ -179,7 +179,7 @@ component Captcha {
|
||||
<div
|
||||
data-captcha-image-wrap
|
||||
hidden
|
||||
class='overflow-hidden rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-white'
|
||||
class='overflow-hidden rounded-[var(--wrn-radius-sm)] border border-[var(--wrn-color-border)] bg-white'
|
||||
>
|
||||
<img
|
||||
data-captcha-image
|
||||
@@ -201,7 +201,7 @@ component Captcha {
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
class='group relative overflow-hidden rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-white p-1 group-data-[captcha-size=compact]/captcha:p-0.5 transition-[border-color,box-shadow,transform] duration-[var(--wire-motion-fast)] hover:border-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] aria-[pressed=true]:border-[var(--captcha-accent)] aria-[pressed=true]:ring-2 aria-[pressed=true]:ring-[var(--captcha-accent)]'
|
||||
class='group relative overflow-hidden rounded-[var(--wrn-radius-sm)] border border-[var(--wrn-color-border)] bg-white p-1 group-data-[captcha-size=compact]/captcha:p-0.5 transition-[border-color,box-shadow,transform] duration-[var(--wrn-motion-fast)] hover:border-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] aria-[pressed=true]:border-[var(--captcha-accent)] aria-[pressed=true]:ring-2 aria-[pressed=true]:ring-[var(--captcha-accent)]'
|
||||
>
|
||||
<img
|
||||
data-captcha-item-image
|
||||
@@ -235,7 +235,7 @@ component Captcha {
|
||||
|
||||
<div data-captcha-response-panel class='flex min-w-0 flex-col gap-2 group-data-[captcha-layout=horizontal]/captcha:self-stretch'>
|
||||
<label data-captcha-answer-wrap hidden class='flex min-w-0 flex-col gap-1.5 group-data-[captcha-size=compact]/captcha:gap-1'>
|
||||
<span class='text-xs font-semibold text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[10px]'>Your answer</span>
|
||||
<span class='text-xs font-semibold text-[var(--wrn-color-text)] group-data-[captcha-size=compact]/captcha:text-[10px]'>Your answer</span>
|
||||
<input
|
||||
data-captcha-answer
|
||||
type="text"
|
||||
@@ -243,22 +243,22 @@ component Captcha {
|
||||
autocomplete="off"
|
||||
autocapitalize="characters"
|
||||
spellcheck="false"
|
||||
class='h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] px-3 text-base font-semibold tracking-[0.18em] text-[var(--wire-color-text)] outline-none transition-[border-color,box-shadow] placeholder:tracking-normal focus:border-[var(--captcha-accent)] focus:ring-2 focus:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:bg-[var(--wire-color-surface-2)] aria-[invalid=true]:border-[var(--wire-color-danger)] aria-[invalid=true]:ring-2 aria-[invalid=true]:ring-[var(--wire-color-danger)] group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:px-2 group-data-[captcha-size=compact]/captcha:text-xs group-data-[captcha-size=compact]/captcha:tracking-[0.12em] group-data-[captcha-size=big]/captcha:h-12 group-data-[captcha-size=big]/captcha:px-4 group-data-[captcha-size=big]/captcha:text-lg'
|
||||
class='h-11 w-full rounded-[var(--wrn-radius-sm)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] px-3 text-base font-semibold tracking-[0.18em] text-[var(--wrn-color-text)] outline-none transition-[border-color,box-shadow] placeholder:tracking-normal focus:border-[var(--captcha-accent)] focus:ring-2 focus:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:bg-[var(--wrn-color-surface-2)] aria-[invalid=true]:border-[var(--wrn-color-danger)] aria-[invalid=true]:ring-2 aria-[invalid=true]:ring-[var(--wrn-color-danger)] group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:px-2 group-data-[captcha-size=compact]/captcha:text-xs group-data-[captcha-size=compact]/captcha:tracking-[0.12em] group-data-[captcha-size=big]/captcha:h-12 group-data-[captcha-size=big]/captcha:px-4 group-data-[captcha-size=big]/captcha:text-lg'
|
||||
placeholder="Enter the answer"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div data-captcha-success hidden role="status" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wire-color-success)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<div data-captcha-success hidden role="status" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wrn-color-success)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<span aria-hidden="true" class='icon-[lucide--circle-check] mt-0.5 size-3.5 shrink-0'></span>
|
||||
<span data-captcha-success-message>Verification completed.</span>
|
||||
</div>
|
||||
|
||||
<div data-captcha-error hidden role="alert" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wire-color-danger)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:mx-3 group-data-[captcha-type=not-robot]/captcha:mb-3'>
|
||||
<div data-captcha-error hidden role="alert" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wrn-color-danger)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:mx-3 group-data-[captcha-type=not-robot]/captcha:mb-3'>
|
||||
<span aria-hidden="true" class='icon-[lucide--circle-alert] mt-0.5 size-3.5 shrink-0'></span>
|
||||
<span data-captcha-error-message></span>
|
||||
</div>
|
||||
|
||||
<p data-captcha-help class='m-0 text-xs leading-5 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<p data-captcha-help class='m-0 text-xs leading-5 text-[var(--wrn-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
{helpText}
|
||||
</p>
|
||||
|
||||
@@ -268,7 +268,7 @@ component Captcha {
|
||||
data-captcha-audio
|
||||
hidden
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wire-color-muted)] transition-colors hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wrn-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wrn-color-muted)] transition-colors hover:bg-[var(--wrn-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--volume-2] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
Listen
|
||||
@@ -278,7 +278,7 @@ component Captcha {
|
||||
data-captcha-audio-alternative
|
||||
hidden
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wire-color-muted)] transition-colors hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wrn-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wrn-color-muted)] transition-colors hover:bg-[var(--wrn-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--ear] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
Use audio
|
||||
@@ -287,7 +287,7 @@ component Captcha {
|
||||
<button
|
||||
data-captcha-refresh
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wire-color-muted)] transition-colors hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wrn-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wrn-color-muted)] transition-colors hover:bg-[var(--wrn-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--refresh-cw] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
New challenge
|
||||
@@ -297,7 +297,7 @@ component Captcha {
|
||||
<button
|
||||
data-captcha-verify
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] bg-[var(--captcha-accent)] px-3 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-2.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-white shadow-sm transition-[filter,transform] hover:brightness-95 active:translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-surface)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wrn-radius-sm)] bg-[var(--captcha-accent)] px-3 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-2.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-white shadow-sm transition-[filter,transform] hover:brightness-95 active:translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wrn-color-surface)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span data-captcha-verify-icon aria-hidden="true" class='icon-[lucide--shield-check] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
<span data-captcha-verify-spinner hidden aria-hidden="true" class='icon-[lucide--loader-circle] size-4 animate-spin group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
@@ -312,7 +312,7 @@ component Captcha {
|
||||
value=""
|
||||
/>
|
||||
|
||||
<p data-captcha-credit class='m-0 text-[11px] leading-4 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<p data-captcha-credit class='m-0 text-[11px] leading-4 text-[var(--wrn-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
Protected by WRNexus CAPTCHA. Server-side verification is required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/captcha",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -23,11 +23,11 @@ test("Captcha browser runtime is valid JavaScript and exposes the expected lifec
|
||||
expect(source).toContain("normalizeSize");
|
||||
expect(source).toContain("verifyNotRobot");
|
||||
expect(source).toContain("stopImmediatePropagation");
|
||||
expect(source).toContain("__wireSubmitGuards");
|
||||
expect(source).toContain("__wireValidateBound");
|
||||
expect(source).toContain("__wrnSubmitGuards");
|
||||
expect(source).toContain("__wrnValidateBound");
|
||||
expect(source).toContain("passesSubmitGuard");
|
||||
expect(source).toContain('addEventListener("submit"');
|
||||
expect(source).toContain('addEventListener("wire:error"');
|
||||
expect(source).toContain('addEventListener("wrn:error"');
|
||||
expect(source).toContain("state.config.resetOnError || captchaFailed");
|
||||
expect(source).toContain('state.status !== "ready" && state.status !== "incorrect"');
|
||||
expect(source).toContain('state.status === "incorrect") setStatus(state, "ready", "")');
|
||||
|
||||
@@ -63,7 +63,7 @@ test("Captcha.wrn parses and exposes the full public contract", async () => {
|
||||
expect(source).toContain("order: -1");
|
||||
expect(source).toContain("data-[captcha-size=big]");
|
||||
expect(source).toContain("{...attrs}");
|
||||
expect(source).toContain("--wire-");
|
||||
expect(source).toContain("--wrn-");
|
||||
expect(source).not.toMatch(/=\{/);
|
||||
expect(source).toContain('data-wrnexus-runtime="captcha"');
|
||||
expect(source).not.toContain("<script");
|
||||
|
||||
@@ -79,7 +79,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
|
||||
| `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. |
|
||||
| `wrnexus generate mobile` | Scaffold a Capacitor shell for iOS and Android. |
|
||||
| `wrnexus mobile add <package...>` | Install Capacitor plugins and sync native projects. |
|
||||
| `wrnexus eject <name...>` | Copy Wire UI component `.wrn` sources into `app/components/`. |
|
||||
| `wrnexus eject <name...>` | Copy WrNexus UI component `.wrn` sources into `app/components/`. |
|
||||
| `wrnexus db <cmd>` | Database migrations and tooling (see [db](#wrnexus-db)). |
|
||||
| `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). |
|
||||
| `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. |
|
||||
@@ -179,7 +179,7 @@ WrNexus response pages.
|
||||
|
||||
### `wrnexus eject`
|
||||
|
||||
Copies a Wire UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app.
|
||||
Copies a WrNexus UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app.
|
||||
|
||||
```bash
|
||||
wrnexus eject button card modal
|
||||
@@ -322,5 +322,5 @@ export default config;
|
||||
## Requirements / Notes
|
||||
|
||||
- **Bun-only.** The CLI runs on Bun, spawns the Bun binary for the dev child and `bun test`, and the production build uses `Bun.build`. Node is not supported.
|
||||
- Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn` → `.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable Wire UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`.
|
||||
- Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn` → `.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable WrNexus UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`.
|
||||
- Reads `wrnexus.config.ts` for `db` / `databases`, `theme`, `styles`, `seo`, `security`, `i18n`, and `profiles`, and `wrnexus.workspace.ts` for the gateway.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.17",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -63,7 +63,7 @@ page Home {
|
||||
}
|
||||
|
||||
style {
|
||||
h1 { color: var(--wire-color-text); }
|
||||
h1 { color: var(--wrn-color-text); }
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
@@ -95,7 +95,7 @@ Components render on the server with their props, then hydrate — no per-compon
|
||||
- **Server loop (DB/list/table):** \`{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}\` — iterates SSR data on the server and renders markup per item. \`{item.field}\` interpolates (HTML-escaped, XSS-safe). \`<list>\` is a JS expression, usually an \`ssr\` data binding (see "Data-driven tables" below). This is how you render a database table in \`.wrn\`.
|
||||
- **Server conditional:** \`{#if <expr>} … {:else if <expr>} … {:else} … {/if}\` — renders the first truthy branch on the server. \`<expr>\` can reference \`ssr\` data, or the \`item\`/\`index\` of an enclosing \`{#each}\`. Works at page level and inside loops (e.g. \`{#if r.active}<span>●</span>{:else}<span>○</span>{/if}\` per row). For client-side show/hide based on reactive \`state\`, use \`data-show="expr"\` instead.
|
||||
- i18n: \`{t:home.title}\` in text, \`t:placeholder="form.name"\` on attributes — resolved per request from \`app/locales/\`.
|
||||
- Theme: any element with \`data-wire-theme-toggle\` toggles light/dark; \`data-wire-theme-set="dark"\` sets it.
|
||||
- Theme: any element with \`data-wrn-theme-toggle\` toggles light/dark; \`data-wrn-theme-set="dark"\` sets it.
|
||||
- Void/self-closing tags are fine: \`<br />\`, \`<img src="..." />\`.
|
||||
- Only \`{\` and \`}\` are special (interpolation). Don't use a bare \`}\` in view text.
|
||||
|
||||
@@ -246,7 +246,7 @@ export default v.object({
|
||||
});
|
||||
\`\`\`
|
||||
In an API route: \`import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);\` → \`r.ok ? r.value : r.response\`.
|
||||
In a form: \`<form data-schema="login" action="/api/login" method="post">\` + \`<span data-error="email"></span>\` (client + server validation wired automatically).
|
||||
In a form: \`<form data-schema="login" action="/api/login" method="post">\` + \`<span data-error="email"></span>\` (client + server validation connected automatically).
|
||||
|
||||
## AI / LLM (\`@wrnexus/ai\`)
|
||||
|
||||
@@ -271,7 +271,7 @@ wrnexus update --latest # deps + syntax/config migrations + verification
|
||||
wrnexus generate page <Name> # scaffold a page (aliases: g p)
|
||||
wrnexus generate component <name> | api <path> | schema <name>
|
||||
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
|
||||
wrnexus eject <component> # copy a Wire UI component's .wrn into app/components to customize
|
||||
wrnexus eject <component> # copy a WrNexus UI component's .wrn into app/components to customize
|
||||
\`\`\`
|
||||
|
||||
## When asked to "create a page/component/feature"
|
||||
@@ -279,7 +279,7 @@ wrnexus eject <component> # copy a Wire UI component's .wrn into app/compone
|
||||
1. Create the \`.wrn\` file under \`app/pages/\` (or \`app/components/\`) with a \`page\`/\`component\` block — or run \`wrnexus generate page <Name>\`.
|
||||
2. Put markup in \`view { }\`, interactive bits in \`state\` + \`{expr}\` + \`@event\`, reusable UI as components mounted via \`data-component\`.
|
||||
3. For data, add an \`app/api/*.ts\` route and \`getDb()\`; for forms, add an \`app/schemas/*.ts\` and \`data-schema\`.
|
||||
4. Style with Tailwind utility classes in the view, or theme tokens (\`var(--wire-*)\`), or \`style { }\`.
|
||||
4. Style with Tailwind utility classes in the view, or theme tokens (\`var(--wrn-*)\`), or \`style { }\`.
|
||||
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.\n`;
|
||||
|
||||
/** Agent-oriented instructions (shipped as CLAUDE.md): a preamble + the full guide. */
|
||||
|
||||
@@ -515,7 +515,7 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
assetHash.update(themeJs);
|
||||
console.log(`✓ Theme: ${theme.names.length} themes (default: ${theme.default})`);
|
||||
|
||||
// 1a2) Wire UI stylesheet (all component classes, themed via tokens).
|
||||
// 1a2) WrNexus UI stylesheet (all component classes, themed via tokens).
|
||||
const uiStyles = config.styles?.includeUi === false ? "" : uiCss();
|
||||
writeFileSync(join(distDir, "ui.css"), uiStyles, "utf8");
|
||||
const frameworkStyles = `${themeCss}\n${uiStyles}`;
|
||||
|
||||
@@ -447,11 +447,11 @@ export const contactSchema = v.object({
|
||||
|
||||
/* Make Tailwind's \`dark:\` variant follow the framework's data-theme attribute
|
||||
* (set on <html> by the theme system), not the OS setting. Any element with
|
||||
* data-wire-theme-toggle flips it. */
|
||||
* data-wrn-theme-toggle flips it. */
|
||||
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
||||
|
||||
body {
|
||||
font-family: var(--wire-font-sans, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif);
|
||||
font-family: var(--wrn-font-sans, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif);
|
||||
}
|
||||
`,
|
||||
"app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected
|
||||
@@ -497,7 +497,7 @@ page Home {
|
||||
<span class="grid h-7 w-7 place-items-center rounded-md bg-gradient-to-br from-indigo-500 to-violet-600 text-sm font-bold text-white">W</span>
|
||||
APP_NAME
|
||||
</span>
|
||||
<button data-wire-theme-toggle class="rounded-md border border-slate-200 px-3 py-1.5 text-sm text-slate-600 transition hover:border-slate-300 hover:text-slate-900 dark:border-white/10 dark:text-slate-400 dark:hover:border-white/20 dark:hover:text-white">
|
||||
<button data-wrn-theme-toggle class="rounded-md border border-slate-200 px-3 py-1.5 text-sm text-slate-600 transition hover:border-slate-300 hover:text-slate-900 dark:border-white/10 dark:text-slate-400 dark:hover:border-white/20 dark:hover:text-white">
|
||||
Toggle theme
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@@ -52,7 +52,7 @@ async function listTables(db: import("@wrnexus/db").Db): Promise<string[]> {
|
||||
? "SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name"
|
||||
: "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name";
|
||||
const rows = await db.all<{ name: string }>(sql);
|
||||
return rows.map((r) => r.name).filter((n) => n !== "_wire_migrations");
|
||||
return rows.map((r) => r.name).filter((n) => n !== "_wrn_migrations");
|
||||
}
|
||||
|
||||
function isModel(value: unknown): value is Model {
|
||||
|
||||
@@ -49,7 +49,7 @@ const COMPOSE = `services:
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: "3000"
|
||||
DATABASE_URL: postgres://wire:wire@db:5432/app
|
||||
DATABASE_URL: postgres://wrn:wrn@db:5432/app
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -58,11 +58,11 @@ const COMPOSE = `services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: wire
|
||||
POSTGRES_PASSWORD: wire
|
||||
POSTGRES_USER: wrn
|
||||
POSTGRES_PASSWORD: wrn
|
||||
POSTGRES_DB: app
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U wire -d app"]
|
||||
test: ["CMD-SHELL", "pg_isready -U wrn -d app"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* `wrnexus eject <name...>` — copy a Wire UI component's `.wrn` source into the
|
||||
* `wrnexus eject <name...>` — copy a WrNexus UI component's `.wrn` source into the
|
||||
* app's `app/components/`, so you fully own and can edit it. The auto-discovered
|
||||
* library version is shadowed by the app copy (same name → app wins).
|
||||
*/
|
||||
|
||||
@@ -75,7 +75,7 @@ export function scaffold(type: GenerateType, name: string): GeneratedFile {
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="wire-${baseName(clean)}">{label}</div>
|
||||
<div class="wrn-${baseName(clean)}">{label}</div>
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* wrnexus dev [app-dir] [--port=3000] start the dev server (live reload)
|
||||
* wrnexus build [app-dir] build a production server + assets
|
||||
* wrnexus create <app-name> scaffold a new app
|
||||
* wrnexus eject <name...> copy a Wire UI component into your app
|
||||
* wrnexus eject <name...> copy a WrNexus UI component into your app
|
||||
* wrnexus db <migrate|rollback|status|new> database migrations
|
||||
* wrnexus authz <list|generate|init> authorization catalog tooling
|
||||
*/
|
||||
@@ -64,7 +64,7 @@ Usage:
|
||||
wrnexus mobile compile Compile .wrn pages into native Expo routes
|
||||
wrnexus native list List cross-platform native capabilities
|
||||
wrnexus native add <capability...> Install capability packages for the configured mobile mode
|
||||
wrnexus eject <name...> Copy a Wire UI component into app/components
|
||||
wrnexus eject <name...> Copy a WrNexus UI component into app/components
|
||||
wrnexus update [dir] [--latest] Upgrade deps, migrate project files, and verify the app
|
||||
wrnexus db <cmd> Migrations: migrate | rollback | status | seed | generate | new
|
||||
wrnexus authz <cmd> Authorization: list | generate | init [--dialect=sqlite|postgres|mysql]
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { loadAppConfig } from "@wrnexus/styles";
|
||||
import { compileNativeWireFile } from "@wrnexus/compiler";
|
||||
import { compileNativeWrnFile } from "@wrnexus/compiler";
|
||||
|
||||
function validPackageName(value: string): boolean {
|
||||
return /^(?:@[a-z0-9._~-]+\/)?[a-z0-9._~-]+(?:@[a-zA-Z0-9._~^<>=|*+-]+)?$/.test(value);
|
||||
@@ -144,7 +144,7 @@ export async function runMobileCommand(
|
||||
const output = join(mobileDir, "app", relative);
|
||||
mkdirSync(resolve(output, ".."), { recursive: true });
|
||||
try {
|
||||
writeFileSync(output, compileNativeWireFile(readFileSync(source, "utf8")), "utf8");
|
||||
writeFileSync(output, compileNativeWrnFile(readFileSync(source, "utf8")), "utf8");
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Native compilation failed for app/pages/${entry}: ${(error as Error).message}`,
|
||||
|
||||
@@ -2064,7 +2064,7 @@ const MIGRATIONS: Migration[] = [
|
||||
version: "0.8.6",
|
||||
id: "0.8.6-navigation-and-layout-groups",
|
||||
description:
|
||||
"Rebuilds the navigation and layout component groups, moves them off Tailwind utilities onto wire-* classes, defines theme tokens that components referenced but nothing declared, and makes component outputs actually reach parent bindings.",
|
||||
"Rebuilds the navigation and layout component groups, moves them off Tailwind utilities onto wrn-* classes, defines theme tokens that components referenced but nothing declared, and makes component outputs actually reach parent bindings.",
|
||||
apply() {
|
||||
// Source changes no codemod can make safely, so they are listed rather
|
||||
// than attempted.
|
||||
@@ -2100,11 +2100,11 @@ const MIGRATIONS: Migration[] = [
|
||||
// move to the @change and @select bindings.
|
||||
//
|
||||
// Sidebar renamed its classes to the BEM form used everywhere else:
|
||||
// wire-sidebar-shell, -items, -group, -toggle, -backdrop, -panel and
|
||||
// -layout became wire-sidebar__*. Nesting via children still works.
|
||||
// wrn-sidebar-shell, -items, -group, -toggle, -backdrop, -panel and
|
||||
// -layout became wrn-sidebar__*. Nesting via children still works.
|
||||
//
|
||||
// The layout, page and section components moved from Tailwind utility
|
||||
// classes to wire-* classes with their own styles. Application CSS
|
||||
// classes to wrn-* classes with their own styles. Application CSS
|
||||
// selecting on the utility classes they used to render -- max-w-7xl,
|
||||
// gap-5, sm:grid-cols-2 and the rest -- no longer matches. Variants are
|
||||
// data attributes now, so target [data-variant] and friends instead.
|
||||
@@ -2118,12 +2118,12 @@ const MIGRATIONS: Migration[] = [
|
||||
// radius.
|
||||
//
|
||||
// ui.css lost several application-pattern class families that nothing
|
||||
// referenced: wire-catalog-*, wire-page-shell, wire-product-card,
|
||||
// wire-legal-toc, wire-sdk-tabs, wire-cookie-* and wire-analytics-preview.
|
||||
// referenced: wrn-catalog-*, wrn-page-shell, wrn-product-card,
|
||||
// wrn-legal-toc, wrn-sdk-tabs, wrn-cookie-* and wrn-analytics-preview.
|
||||
// Anything hand-written against those needs its own styles.
|
||||
//
|
||||
// Themes gain tokens that were referenced but never defined, including
|
||||
// --wire-color-focus, --wire-color-surface-soft, --wire-color-on-danger
|
||||
// --wrn-color-focus, --wrn-color-surface-soft, --wrn-color-on-danger
|
||||
// and the input-* family. A custom theme that declared these itself
|
||||
// keeps winning; one that did not will see focus rings and soft surfaces
|
||||
// start painting where they previously rendered as nothing.
|
||||
@@ -2133,7 +2133,7 @@ const MIGRATIONS: Migration[] = [
|
||||
version: "0.8.7",
|
||||
id: "0.8.7-ui-component-depth",
|
||||
description:
|
||||
"Removes superseded UI scaffolds and moves the remaining utility-styled components to self-contained wire-* BEM styles.",
|
||||
"Removes superseded UI scaffolds and moves the remaining utility-styled components to self-contained wrn-* BEM styles.",
|
||||
apply() {
|
||||
// AdvancedDatePicker -> DatePicker; AdvancedRangeSlider -> RangeSlider;
|
||||
// FileUpload -> FileInput + FileUploadProgress (or @wrnexus/uploader);
|
||||
@@ -2141,7 +2141,7 @@ const MIGRATIONS: Migration[] = [
|
||||
//
|
||||
// List, InputNumber, Marquee, TextLink, Map, SearchBox and Timeline no
|
||||
// longer render Tailwind utility class names. Application CSS selecting
|
||||
// those internal utilities must move to the component's wire-* BEM
|
||||
// those internal utilities must move to the component's wrn-* BEM
|
||||
// classes or its data-size/data-variant attributes. List now emits
|
||||
// select for non-link items as well as navigation items.
|
||||
},
|
||||
|
||||
@@ -151,7 +151,7 @@ test("scaffoldApp uses compatible framework packages and pins the current CLI",
|
||||
const globalCss = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
|
||||
expect(globalCss).toContain('@plugin "@iconify/tailwind4";');
|
||||
expect(globalCss).toContain('"Plus Jakarta Sans"');
|
||||
expect(globalCss).toContain("--wire-font-sans");
|
||||
expect(globalCss).toContain("--wrn-font-sans");
|
||||
const appConfig = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||
expect(appConfig).toContain('family: "Plus Jakarta Sans"');
|
||||
expect(appConfig).toContain("weights: [400, 500, 600, 700]");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
||||
import { generateDocker } from "../src/docker.ts";
|
||||
|
||||
test("generateDocker scaffolds Dockerfile, .dockerignore, compose", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-docker-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-docker-"));
|
||||
generateDocker(dir);
|
||||
expect(existsSync(join(dir, "Dockerfile"))).toBe(true);
|
||||
expect(existsSync(join(dir, ".dockerignore"))).toBe(true);
|
||||
@@ -21,7 +21,7 @@ test("generateDocker scaffolds Dockerfile, .dockerignore, compose", () => {
|
||||
});
|
||||
|
||||
test("generateDocker does not overwrite existing files", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-docker-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-docker-"));
|
||||
generateDocker(dir);
|
||||
const before = readFileSync(join(dir, "Dockerfile"), "utf8");
|
||||
// second run must leave the file untouched
|
||||
|
||||
@@ -12,7 +12,7 @@ test("scaffold component: props + class", () => {
|
||||
const f = scaffold("component", "user-card");
|
||||
expect(f.path).toBe("components/user-card.wrn");
|
||||
expect(f.content).toContain("component UserCard {");
|
||||
expect(f.content).toContain('class="wire-user-card"');
|
||||
expect(f.content).toContain('class="wrn-user-card"');
|
||||
});
|
||||
|
||||
test("scaffold api: nested path keeps directories, exports GET", () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { runMobileCommand } from "../src/mobile-command.ts";
|
||||
import { nativeCatalog, runNativeCommand } from "../src/native-command.ts";
|
||||
|
||||
test("generateMobile scaffolds a configurable Capacitor project", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-mobile-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-mobile-"));
|
||||
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "@acme/store-front" }));
|
||||
|
||||
await generateMobile(dir, {
|
||||
@@ -30,7 +30,7 @@ test("generateMobile scaffolds a configurable Capacitor project", async () => {
|
||||
});
|
||||
|
||||
test("generateMobile derives defaults and does not overwrite files", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-mobile-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-mobile-"));
|
||||
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "demo-app" }));
|
||||
await generateMobile(dir);
|
||||
const path = join(dir, "mobile", "capacitor.config.ts");
|
||||
@@ -53,7 +53,7 @@ test("mobileOptions parses generator flags", () => {
|
||||
});
|
||||
|
||||
test("generateMobile scaffolds a WebView-free native project from config", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-native-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-native-"));
|
||||
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "native-demo" }));
|
||||
writeFileSync(
|
||||
join(dir, "wrnexus.config.mjs"),
|
||||
@@ -71,7 +71,7 @@ test("generateMobile scaffolds a WebView-free native project from config", async
|
||||
});
|
||||
|
||||
test("mobile compile turns wrn pages into Expo routes", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-native-compile-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-native-compile-"));
|
||||
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "native-compile" }));
|
||||
writeFileSync(join(dir, "wrnexus.config.mjs"), `export default { mobile: { mode: "native" } }`);
|
||||
await generateMobile(dir);
|
||||
@@ -90,7 +90,7 @@ test("mobile compile turns wrn pages into Expo routes", async () => {
|
||||
|
||||
test("native catalog uses built-in React Native sharing without installing expo-sharing", async () => {
|
||||
expect(nativeCatalog.share?.expo).toBeUndefined();
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-native-share-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-native-share-"));
|
||||
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "native-share" }));
|
||||
writeFileSync(join(dir, "wrnexus.config.mjs"), `export default { mobile: { mode: "native" } }`);
|
||||
await generateMobile(dir);
|
||||
|
||||
@@ -69,7 +69,7 @@ bun add @wrnexus/compiler
|
||||
|
||||
All exports come from the package root (`@wrnexus/compiler`).
|
||||
|
||||
### `compileWireFile(source: string): string`
|
||||
### `compileWrnFile(source: string): string`
|
||||
|
||||
Compile `.wrn` source to a TypeScript module string. Throws `ParseError` on invalid input. The output is prefixed with a `// compiled from .wrn` comment.
|
||||
|
||||
@@ -115,10 +115,10 @@ class Lexer {
|
||||
|
||||
### Errors
|
||||
|
||||
| Class | Thrown by | Meaning |
|
||||
| ------------ | ------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `ParseError` | `parse`, `compile`, `compileWireFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. |
|
||||
| `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. |
|
||||
| Class | Thrown by | Meaning |
|
||||
| ------------ | ------------------------------------------------ | --------------------------------------------------------------- |
|
||||
| `ParseError` | `parse`, `compile`, `compileWrnFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. |
|
||||
| `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. |
|
||||
|
||||
### AST types
|
||||
|
||||
@@ -143,9 +143,9 @@ Exported type-only symbols describing the parsed tree:
|
||||
Compile a page:
|
||||
|
||||
```ts
|
||||
import { compileWireFile } from "@wrnexus/compiler";
|
||||
import { compileWrnFile } from "@wrnexus/compiler";
|
||||
|
||||
const ts = compileWireFile(`
|
||||
const ts = compileWrnFile(`
|
||||
page Home {
|
||||
state count = 0
|
||||
seo { title = "Home" description = "Welcome" }
|
||||
@@ -214,4 +214,4 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
|
||||
## Requirements / Notes
|
||||
|
||||
- Pure TypeScript with no runtime dependencies; runs under **Bun** as part of the WrNexus toolchain (Node is not supported).
|
||||
- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wire*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.
|
||||
- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wrn*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -508,7 +508,7 @@ function compileIfExpr(node: IfNode): string {
|
||||
|
||||
/**
|
||||
* Collect every server-control expression in a view (recursively): `{#each}` list
|
||||
* expressions and `{#if}` conditions. Used to wire up raw SSR data consts.
|
||||
* expressions and `{#if}` conditions. Used to wrn up raw SSR data consts.
|
||||
*/
|
||||
function collectControlExprs(nodes: ViewNode[], out: string[] = []): string[] {
|
||||
for (const node of nodes) {
|
||||
@@ -727,7 +727,7 @@ function renderNestedComponentInvocation(
|
||||
.map((attr) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
||||
if (spread) {
|
||||
return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1]!)})}`;
|
||||
return `\${__wrnSpreadAttrs(${ctx.resolveExpr(spread[1]!)})}`;
|
||||
}
|
||||
|
||||
if (attr.event) {
|
||||
@@ -745,7 +745,7 @@ function renderNestedComponentInvocation(
|
||||
const wholeExpression = wholeAttributeExpression(attr.value);
|
||||
|
||||
const compiledValue = wholeExpression
|
||||
? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}`
|
||||
? `\${__wrnProp(${ctx.resolveExpr(wholeExpression)})}`
|
||||
: compileAttrValue(attr.value, ctx);
|
||||
|
||||
const rendered = ` ${attr.name}="${compiledValue}"`;
|
||||
@@ -778,7 +778,7 @@ function renderNestedComponentInvocation(
|
||||
|
||||
return (
|
||||
`<div data-component="${attrEscape(node.tag)}"` +
|
||||
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
||||
`${ctx.forwardRestAttrs ? "${__wrnSpreadAttrs(__attrs)}" : ""}` +
|
||||
`${attrs}>${inner}</div>`
|
||||
);
|
||||
}
|
||||
@@ -1910,7 +1910,7 @@ function viewHasRestAttributeSpread(nodes: ViewNode[]): boolean {
|
||||
/**
|
||||
* Compile a text node. Interpolations that reference state stay as client
|
||||
* mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of
|
||||
* props/constants are baked server-side (`${__wireHtml(expr)}`), so static
|
||||
* props/constants are baked server-side (`${__wrnHtml(expr)}`), so static
|
||||
* components render correct HTML with zero JavaScript.
|
||||
*/
|
||||
function compileText(raw: string, ctx: CompCtx): string {
|
||||
@@ -1929,17 +1929,17 @@ function compileText(raw: string, ctx: CompCtx): string {
|
||||
// list renderer fills it per item; it has no server-side value.
|
||||
out += escLit(`{${expr}}`);
|
||||
} else if (expr === "content") {
|
||||
out += `\${__wireRaw(${ctx.resolveExpr(expr)})}`;
|
||||
out += `\${__wrnRaw(${ctx.resolveExpr(expr)})}`;
|
||||
} else if (exprRefsComponentReactiveValue(expr, ctx)) {
|
||||
// State interpolation: bake the initial value AND keep it reactive via a
|
||||
// data-text span, so no-JS clients see the real value and hydration
|
||||
// updates it in place. `count` → `<span data-text="count">0</span>`.
|
||||
out +=
|
||||
escLit(`<span data-text="${attrEscape(expr)}">`) +
|
||||
`\${__wireHtml(${ctx.resolveExpr(expr)})}` +
|
||||
`\${__wrnHtml(${ctx.resolveExpr(expr)})}` +
|
||||
escLit(`</span>`);
|
||||
} else {
|
||||
out += `\${__wireHtml(${ctx.resolveExpr(expr)})}`;
|
||||
out += `\${__wrnHtml(${ctx.resolveExpr(expr)})}`;
|
||||
}
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
@@ -1959,7 +1959,7 @@ function compileAttrValue(raw: string, ctx: CompCtx): string {
|
||||
if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
|
||||
out += escLit(`{${expr}}`); // hydrated per-item by the list renderer
|
||||
} else {
|
||||
out += `\${__wireAttr(${ctx.resolveExpr(expr)})}`;
|
||||
out += `\${__wrnAttr(${ctx.resolveExpr(expr)})}`;
|
||||
}
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
@@ -2152,7 +2152,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
.map((a) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
||||
if (spread) {
|
||||
return `\${__wireSpreadAttrs(${elementContext.resolveExpr(spread[1]!)})}`;
|
||||
return `\${__wrnSpreadAttrs(${elementContext.resolveExpr(spread[1]!)})}`;
|
||||
}
|
||||
|
||||
if (a.event) {
|
||||
@@ -2181,7 +2181,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
: "";
|
||||
/*
|
||||
* A boolean attribute whose expression names a loop variable cannot
|
||||
* be resolved on the server: __wireBooleanAttr runs at render time,
|
||||
* be resolved on the server: __wrnBooleanAttr runs at render time,
|
||||
* where `row` or `item` simply does not exist, and the emitted
|
||||
* module blew up. Leave the attribute off the server output and let
|
||||
* the client bind set it -- the runtime toggles boolean attributes
|
||||
@@ -2193,7 +2193,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
attrEscape(JSON.stringify([a.name, a.value])),
|
||||
)}"`;
|
||||
}
|
||||
return `\${__wireBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker}`;
|
||||
return `\${__wrnBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker}`;
|
||||
}
|
||||
|
||||
if (a.value === "false") return "";
|
||||
@@ -2219,7 +2219,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
|
||||
const compiledValue =
|
||||
isExplicitComponentMount && wholeExpression
|
||||
? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||
: compileAttrValue(a.value, elementContext);
|
||||
const rendered = ` ${a.name}="${compiledValue}"`;
|
||||
|
||||
@@ -2297,7 +2297,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
|
||||
const allAttrs =
|
||||
`${loopLocalsAttribute}` +
|
||||
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
||||
`${ctx.forwardRestAttrs ? "${__wrnSpreadAttrs(__attrs)}" : ""}` +
|
||||
`${
|
||||
ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""
|
||||
}` +
|
||||
@@ -2584,7 +2584,7 @@ function __restProps(
|
||||
);
|
||||
}
|
||||
|
||||
function __wireHtml(v: unknown): string {
|
||||
function __wrnHtml(v: unknown): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>]/g,
|
||||
(c) =>
|
||||
@@ -2596,7 +2596,7 @@ function __wireHtml(v: unknown): string {
|
||||
);
|
||||
}
|
||||
|
||||
function __wireAttr(v: unknown): string {
|
||||
function __wrnAttr(v: unknown): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>"]/g,
|
||||
(c) =>
|
||||
@@ -2610,7 +2610,7 @@ function __wireAttr(v: unknown): string {
|
||||
);
|
||||
}
|
||||
|
||||
function __wireBooleanAttr(name: string, value: unknown): string {
|
||||
function __wrnBooleanAttr(name: string, value: unknown): string {
|
||||
return value === true ||
|
||||
value === "true" ||
|
||||
value === "" ||
|
||||
@@ -2621,7 +2621,7 @@ function __wireBooleanAttr(name: string, value: unknown): string {
|
||||
: "";
|
||||
}
|
||||
|
||||
function __wireSpreadAttrs(value: unknown): string {
|
||||
function __wrnSpreadAttrs(value: unknown): string {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
|
||||
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
||||
@@ -2646,27 +2646,27 @@ function __wireSpreadAttrs(value: unknown): string {
|
||||
}
|
||||
|
||||
if (booleanAttributes.has(lowerName)) {
|
||||
attributes.push(__wireBooleanAttr(name, raw));
|
||||
attributes.push(__wrnBooleanAttr(name, raw));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw === false || raw === null || raw === undefined) continue;
|
||||
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
|
||||
attributes.push(" " + name + '="' + __wrnAttr(raw) + '"');
|
||||
}
|
||||
|
||||
return attributes.join("");
|
||||
}
|
||||
|
||||
function __wireProp(v: unknown): string {
|
||||
function __wrnProp(v: unknown): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object"
|
||||
? JSON.stringify(v)
|
||||
: String(v == null ? "" : v);
|
||||
|
||||
return __wireAttr(value);
|
||||
return __wrnAttr(value);
|
||||
}
|
||||
|
||||
function __wireRaw(v: unknown): string {
|
||||
function __wrnRaw(v: unknown): string {
|
||||
return String(v == null ? "" : v);
|
||||
}`);
|
||||
|
||||
@@ -2747,7 +2747,7 @@ function __wireRaw(v: unknown): string {
|
||||
return out.join("\n\n") + "\n";
|
||||
}
|
||||
|
||||
function __wireRaw(v: unknown): string {
|
||||
function __wrnRaw(v: unknown): string {
|
||||
return String(v == null ? "" : v);
|
||||
}
|
||||
|
||||
@@ -2756,23 +2756,23 @@ function wholeAttributeExpression(value: string): string | null {
|
||||
|
||||
return match?.[1]?.trim() || null;
|
||||
}
|
||||
function __wireHtml(v: unknown): string {
|
||||
function __wrnHtml(v: unknown): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>]/g, (c) =>
|
||||
c === "&" ? "&" : c === "<" ? "<" : ">",
|
||||
);
|
||||
}
|
||||
|
||||
function __wireAttr(v: unknown): string {
|
||||
function __wrnAttr(v: unknown): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
||||
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
||||
);
|
||||
}
|
||||
|
||||
function __wireProp(v: unknown): string {
|
||||
function __wrnProp(v: unknown): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
||||
|
||||
return __wireAttr(value);
|
||||
return __wrnAttr(value);
|
||||
}
|
||||
function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): string {
|
||||
if (attr.event) {
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface CompileResult {
|
||||
}
|
||||
|
||||
/** Compile `.wrn` source into an Expo Router React Native screen. */
|
||||
export function compileNativeWireFile(source: string): string {
|
||||
export function compileNativeWrnFile(source: string): string {
|
||||
const ast = parse(source);
|
||||
assertValidAst(ast);
|
||||
return generateNative(ast);
|
||||
@@ -93,7 +93,7 @@ export function compileNativeWireFile(source: string): string {
|
||||
* Compile `.wrn` source into TypeScript source. Errors include a stable code,
|
||||
* source location, code frame, and actionable hint whenever available.
|
||||
*/
|
||||
export function compileWireFile(source: string, filePath = "<inline .wrn>"): string {
|
||||
export function compileWrnFile(source: string, filePath = "<inline .wrn>"): string {
|
||||
try {
|
||||
const ast = parse(source);
|
||||
assertValidAst(ast, { file: filePath, accessibility: true });
|
||||
|
||||
@@ -204,7 +204,7 @@ function __diagnostic(code, message, details) {
|
||||
}
|
||||
function __csrfToken() {
|
||||
if (typeof document === "undefined") return undefined;
|
||||
const match = /(?:^|;\\s*)wire-csrf=([^;]+)/.exec(document.cookie || "");
|
||||
const match = /(?:^|;\\s*)wrn-csrf=([^;]+)/.exec(document.cookie || "");
|
||||
return match ? decodeURIComponent(match[1]) : undefined;
|
||||
}
|
||||
async function __callServerFunction(storeName, functionName, args, options) {
|
||||
|
||||
@@ -121,7 +121,7 @@ function __restProps(
|
||||
);
|
||||
}
|
||||
|
||||
function __wireHtml(v: unknown): string {
|
||||
function __wrnHtml(v: unknown): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>]/g,
|
||||
(c) =>
|
||||
@@ -133,7 +133,7 @@ function __wireHtml(v: unknown): string {
|
||||
);
|
||||
}
|
||||
|
||||
function __wireAttr(v: unknown): string {
|
||||
function __wrnAttr(v: unknown): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>"]/g,
|
||||
(c) =>
|
||||
@@ -147,7 +147,7 @@ function __wireAttr(v: unknown): string {
|
||||
);
|
||||
}
|
||||
|
||||
function __wireBooleanAttr(name: string, value: unknown): string {
|
||||
function __wrnBooleanAttr(name: string, value: unknown): string {
|
||||
return value === true ||
|
||||
value === "true" ||
|
||||
value === "" ||
|
||||
@@ -158,7 +158,7 @@ function __wireBooleanAttr(name: string, value: unknown): string {
|
||||
: "";
|
||||
}
|
||||
|
||||
function __wireSpreadAttrs(value: unknown): string {
|
||||
function __wrnSpreadAttrs(value: unknown): string {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
|
||||
const booleanAttributes = new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);
|
||||
@@ -183,27 +183,27 @@ function __wireSpreadAttrs(value: unknown): string {
|
||||
}
|
||||
|
||||
if (booleanAttributes.has(lowerName)) {
|
||||
attributes.push(__wireBooleanAttr(name, raw));
|
||||
attributes.push(__wrnBooleanAttr(name, raw));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw === false || raw === null || raw === undefined) continue;
|
||||
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
|
||||
attributes.push(" " + name + '="' + __wrnAttr(raw) + '"');
|
||||
}
|
||||
|
||||
return attributes.join("");
|
||||
}
|
||||
|
||||
function __wireProp(v: unknown): string {
|
||||
function __wrnProp(v: unknown): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object"
|
||||
? JSON.stringify(v)
|
||||
: String(v == null ? "" : v);
|
||||
|
||||
return __wireAttr(value);
|
||||
return __wrnAttr(value);
|
||||
}
|
||||
|
||||
function __wireRaw(v: unknown): string {
|
||||
function __wrnRaw(v: unknown): string {
|
||||
return String(v == null ? "" : v);
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export function render(props: CounterProps = {} as CounterProps): string {
|
||||
const __scope = __wrnexusScopeDecl(__scopeState);
|
||||
const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");
|
||||
return \`<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}" data-wrn-behavior="eyJmdW5jdGlvbnMiOiJmdW5jdGlvbiBpbmNyZW1lbnQoKXtcbiAgICAgIGNvdW50ID0gY291bnQgKyAxXG4gICAgICBvdXRwdXQuY2hhbmdlKGNvdW50KVxuICAgIH0iLCJvdXRwdXRzIjpbeyJuYW1lIjoiY2hhbmdlIiwicGF5bG9hZCI6eyJuYW1lIjoidmFsdWUiLCJ2YWx1ZVR5cGUiOiJudW1iZXIiLCJvcHRpb25hbCI6ZmFsc2V9fV0sImNvbXB1dGVkIjpbXSwiZWZmZWN0cyI6W10sImxpZmVjeWNsZSI6e30sIndhdGNoZXMiOltdfQ==" data-wrn-hydration="Counter:1skggk6" data-wrn-hydrate="load" data-wrn-runtime="universal" data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__">
|
||||
<div data-component="Button"\${__wireSpreadAttrs(__attrs)} on:click="\${__wireProp(increment)}"><span data-text="label">\${__wireHtml(label)}</span>: <span data-text="count">\${__wireHtml(count)}</span></div>
|
||||
<div data-component="Button"\${__wrnSpreadAttrs(__attrs)} on:click="\${__wrnProp(increment)}"><span data-text="label">\${__wrnHtml(label)}</span>: <span data-text="count">\${__wrnHtml(count)}</span></div>
|
||||
</div>\`;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { generate, parse } from "../src/index.ts";
|
||||
import { compileWireFile } from "../src/index.ts";
|
||||
import { compileWrnFile } from "../src/index.ts";
|
||||
import { mountHtml } from "@wrnexus/test";
|
||||
|
||||
test("explicit static rendering disables hydration metadata", () => {
|
||||
const output = compileWireFile(`page StaticPage {
|
||||
const output = compileWrnFile(`page StaticPage {
|
||||
render = "static"
|
||||
state count = 0
|
||||
view { <button @click="count++">{count}</button> }
|
||||
@@ -19,7 +19,7 @@ test("explicit static rendering disables hydration metadata", () => {
|
||||
});
|
||||
|
||||
test("named data loads compile as parallel typed data entries", () => {
|
||||
const output = compileWireFile(`page Users {
|
||||
const output = compileWrnFile(`page Users {
|
||||
load users { return ["Ada"] }
|
||||
load server teams { return ["Core"] }
|
||||
view { <p>Users</p> }
|
||||
@@ -31,10 +31,10 @@ test("named data loads compile as parallel typed data entries", () => {
|
||||
let seq = 0;
|
||||
/** Compile a `.wrn` source and import the resulting module. */
|
||||
async function compileAndImport(src: string): Promise<Record<string, unknown>> {
|
||||
const dir = join(tmpdir(), "wire-compiler-test");
|
||||
const dir = join(tmpdir(), "wrn-compiler-test");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const file = join(dir, `m${seq++}.ts`);
|
||||
writeFileSync(file, compileWireFile(src));
|
||||
writeFileSync(file, compileWrnFile(src));
|
||||
return import(pathToFileURL(file).href);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ test("component event declarations compile to public root metadata", async () =>
|
||||
});
|
||||
|
||||
test("explicit nested component mounts serialize structured expression props", () => {
|
||||
const output = compileWireFile(`component Shell {
|
||||
const output = compileWrnFile(`component Shell {
|
||||
state brand = { "label": "WRNexus" }
|
||||
state items = [{ "label": "Home", "href": "/" }]
|
||||
view {
|
||||
@@ -129,8 +129,8 @@ test("explicit nested component mounts serialize structured expression props", (
|
||||
}`);
|
||||
|
||||
expect(output).toContain('data-component="Navbar"');
|
||||
expect(output).toContain('brand="${__wireProp(brand)}"');
|
||||
expect(output).toContain('items="${__wireProp(items)}"');
|
||||
expect(output).toContain('brand="${__wrnProp(brand)}"');
|
||||
expect(output).toContain('items="${__wrnProp(items)}"');
|
||||
});
|
||||
|
||||
test("typed props, required props, state, custom types, and function parameters compile", () => {
|
||||
@@ -212,7 +212,7 @@ test("HTML view: void elements, boolean attrs, comments, lone <", () => {
|
||||
const ast = parse(
|
||||
`component T {\n view {\n <input type="text" disabled>\n <br/>\n <!-- comment -->\n <p>a < b</p>\n }\n}`,
|
||||
);
|
||||
const html = compileWireFile(
|
||||
const html = compileWrnFile(
|
||||
`component T {\n view {\n <input type="text" disabled>\n <br/>\n <!-- comment -->\n <p>a < b</p>\n }\n}`,
|
||||
);
|
||||
expect(html).toContain("<input");
|
||||
@@ -310,11 +310,11 @@ test("an explicit attrs spread overrides automatic root forwarding", async () =>
|
||||
|
||||
test("prop-driven component renders SSR content and exposes reactive prop signals", async () => {
|
||||
const mod = await compileAndImport(
|
||||
`component Button {\n props {\n label = "Button"\n variant = "default"\n class = ""\n }\n view { <button class="wire-btn wire-btn--{variant} {class}">{label}</button> }\n}`,
|
||||
`component Button {\n props {\n label = "Button"\n variant = "default"\n class = ""\n }\n view { <button class="wrn-btn wrn-btn--{variant} {class}">{label}</button> }\n}`,
|
||||
);
|
||||
const render = mod.render as (p: Record<string, string>) => string;
|
||||
const out = render({ label: "Save <b>", variant: "primary", class: "mt-2" });
|
||||
expect(out).toContain('class="wire-btn wire-btn--primary mt-2"');
|
||||
expect(out).toContain('class="wrn-btn wrn-btn--primary mt-2"');
|
||||
expect(out).toContain("Save <b>"); // html-escaped
|
||||
expect(out).toContain("data-scope");
|
||||
expect(out).toContain('data-text="label"');
|
||||
@@ -353,7 +353,7 @@ test("prop type coercion follows the default value's type", async () => {
|
||||
});
|
||||
|
||||
test("platform events compile through the native runtime bridge", () => {
|
||||
const out = compileWireFile(`page Platform {
|
||||
const out = compileWrnFile(`page Platform {
|
||||
state count = 0
|
||||
view {
|
||||
<button @browser-click="count++" @mobile-click="count = count + 2">Run</button>
|
||||
@@ -364,20 +364,20 @@ test("platform events compile through the native runtime bridge", () => {
|
||||
});
|
||||
|
||||
test("{t:key} compiles to a data-t marker (both pages and components)", () => {
|
||||
const page = compileWireFile(`page P {\n view { <h1>{t:home.title}</h1> }\n}`);
|
||||
const page = compileWrnFile(`page P {\n view { <h1>{t:home.title}</h1> }\n}`);
|
||||
expect(page).toContain('<span data-t="home.title"></span>');
|
||||
const comp = compileWireFile(`component C {\n view { <h1>{t:x}</h1> }\n}`);
|
||||
const comp = compileWrnFile(`component C {\n view { <h1>{t:x}</h1> }\n}`);
|
||||
expect(comp).toContain('<span data-t="x"></span>');
|
||||
});
|
||||
|
||||
test("{t:} does NOT wrap a page in a data-scope (regression)", () => {
|
||||
// Only state / events force a reactive scope, not i18n markers.
|
||||
const page = compileWireFile(`page P {\n view { <h1>{t:a}</h1> <p>{t:b}</p> }\n}`);
|
||||
const page = compileWrnFile(`page P {\n view { <h1>{t:a}</h1> <p>{t:b}</p> }\n}`);
|
||||
expect(page).not.toContain("data-scope");
|
||||
});
|
||||
|
||||
test("page state text bakes its initial value into a reactive data-text span", () => {
|
||||
const page = compileWireFile(
|
||||
const page = compileWrnFile(
|
||||
`page Reactive {\n state count = 3\n view { <p>Count is {count}, doubled {count * 2}</p> }\n}`,
|
||||
);
|
||||
expect(page).toContain('<span data-text="count">3</span>'); // no-JS sees "3"
|
||||
@@ -386,7 +386,7 @@ test("page state text bakes its initial value into a reactive data-text span", (
|
||||
});
|
||||
|
||||
test("page state attributes bake their initial value and retain a reactive binding", () => {
|
||||
const page = compileWireFile(`page Password {
|
||||
const page = compileWrnFile(`page Password {
|
||||
state show = false
|
||||
view {
|
||||
<input type="{show ? 'text' : 'password'}" aria-label="{show ? 'Hide' : 'Show'}">
|
||||
@@ -418,18 +418,18 @@ test("request-dependent page state attributes resolve during server rendering",
|
||||
});
|
||||
|
||||
test("data-for: loop-variable mustaches stay literal (not baked server-side)", () => {
|
||||
const comp = compileWireFile(
|
||||
const comp = compileWrnFile(
|
||||
`component TodoList {\n state todos = []\n view { <ul><li data-for="t in todos">{t.text}</li></ul> }\n}`,
|
||||
);
|
||||
// The <li> template keeps `{t.text}` for the client list renderer, and the
|
||||
// loop variable is never baked (which would be a server-side ReferenceError).
|
||||
expect(comp).toContain('data-for="t in todos"');
|
||||
expect(comp).toContain("{t.text}");
|
||||
expect(comp).not.toContain("__wireHtml(t.text)");
|
||||
expect(comp).not.toContain("__wrnHtml(t.text)");
|
||||
});
|
||||
|
||||
test("named slots pass through to the component output", () => {
|
||||
const out = compileWireFile(
|
||||
const out = compileWrnFile(
|
||||
`component Card {\n view { <div><slot name="header"></slot><slot></slot></div> }\n}`,
|
||||
);
|
||||
expect(out).toContain('<slot name="header">');
|
||||
@@ -497,9 +497,9 @@ component ParentCard {
|
||||
|
||||
expect(output).toContain('data-component="ChildCard"');
|
||||
|
||||
expect(output).toContain('title="${__wireProp(title)}"');
|
||||
expect(output).toContain('title="${__wrnProp(title)}"');
|
||||
|
||||
expect(output).toContain("function __wireProp");
|
||||
expect(output).toContain("function __wrnProp");
|
||||
});
|
||||
|
||||
test("native array and object props serialize through component mounts", async () => {
|
||||
@@ -561,7 +561,7 @@ layout AppLayout {
|
||||
|
||||
expect(output).toContain('export const __wrnexusLayout = "AppLayout"');
|
||||
|
||||
expect(output).toContain("${__wireRaw(content)}");
|
||||
expect(output).toContain("${__wrnRaw(content)}");
|
||||
|
||||
expect(output).toContain("export function render");
|
||||
});
|
||||
@@ -1298,7 +1298,7 @@ test("WRN 0.3 metadata, computed values, loaders, and actions compile additively
|
||||
action save(input) { return input }
|
||||
view { <button @click="count++">{doubled}</button> }
|
||||
}`;
|
||||
const output = compileWireFile(source);
|
||||
const output = compileWrnFile(source);
|
||||
|
||||
expect(output).toContain('export const __wrnexusRuntime = "universal"');
|
||||
expect(output).toContain('export const __wrnexusHydrate = "idle"');
|
||||
@@ -1393,15 +1393,15 @@ page Pricing {
|
||||
});
|
||||
|
||||
test("style blocks compile with stable page/component/layout metadata", () => {
|
||||
const page = compileWireFile(`page StyledPage {
|
||||
const page = compileWrnFile(`page StyledPage {
|
||||
style { .shared { color: red; } }
|
||||
view { <main class="shared">Page</main> }
|
||||
}`);
|
||||
const component = compileWireFile(`component StyledCard {
|
||||
const component = compileWrnFile(`component StyledCard {
|
||||
style { .shared { color: blue; } }
|
||||
view { <article class="shared">Card</article> }
|
||||
}`);
|
||||
const layout = compileWireFile(`layout StyledLayout {
|
||||
const layout = compileWrnFile(`layout StyledLayout {
|
||||
style { .shared { color: green; } }
|
||||
view { <div class="shared"><slot></slot></div> }
|
||||
}`);
|
||||
@@ -1416,7 +1416,7 @@ test("style blocks compile with stable page/component/layout metadata", () => {
|
||||
});
|
||||
|
||||
test("strict-mode reserved prop names compile through safe local references", () => {
|
||||
const output = compileWireFile(`component Visibility {
|
||||
const output = compileWrnFile(`component Visibility {
|
||||
props { private: boolean = false }
|
||||
view { {#if private}<span>Private</span>{/if} }
|
||||
}`);
|
||||
@@ -1441,7 +1441,7 @@ test("data-show keeps its expression instead of being interpolated away", () =>
|
||||
}
|
||||
}
|
||||
`;
|
||||
const code = compileWireFile(source, "Panel.wrn");
|
||||
const code = compileWrnFile(source, "Panel.wrn");
|
||||
expect(code).toContain(`data-show="open || visible"`);
|
||||
expect(code).not.toContain(`data-show="false"`);
|
||||
});
|
||||
@@ -1467,14 +1467,14 @@ test("a local variable may shadow a prop without breaking the module", () => {
|
||||
}
|
||||
}
|
||||
`;
|
||||
const code = compileWireFile(source, "Sized.wrn");
|
||||
const code = compileWrnFile(source, "Sized.wrn");
|
||||
// The alias must be dropped, not emitted alongside the local declaration.
|
||||
expect(code).not.toContain("const { size } = context.props;");
|
||||
expect(code).toContain("var size = 4");
|
||||
});
|
||||
|
||||
// A boolean attribute whose expression names a loop variable cannot be
|
||||
// resolved on the server -- __wireBooleanAttr runs at render time, where the
|
||||
// resolved on the server -- __wrnBooleanAttr runs at render time, where the
|
||||
// loop variable does not exist, and the generated module failed outright.
|
||||
test("a boolean attribute bound to a loop variable binds on the client", () => {
|
||||
const source = `component Picker {
|
||||
@@ -1493,14 +1493,14 @@ test("a boolean attribute bound to a loop variable binds on the client", () => {
|
||||
}
|
||||
}
|
||||
`;
|
||||
const code = compileWireFile(source, "Picker.wrn");
|
||||
expect(code).not.toContain('__wireBooleanAttr("checked"');
|
||||
const code = compileWrnFile(source, "Picker.wrn");
|
||||
expect(code).not.toContain('__wrnBooleanAttr("checked"');
|
||||
expect(code).toContain("data-wrn-bind-");
|
||||
});
|
||||
|
||||
test("block comments inside props report the authoring restriction", () => {
|
||||
expect(() =>
|
||||
compileWireFile(`component Example {
|
||||
compileWrnFile(`component Example {
|
||||
props {
|
||||
/* use a line comment */
|
||||
label: string = "Example"
|
||||
@@ -1512,7 +1512,7 @@ test("block comments inside props report the authoring restriction", () => {
|
||||
|
||||
test("state page reports its collision with the page keyword", () => {
|
||||
expect(() =>
|
||||
compileWireFile(`page Example {
|
||||
compileWrnFile(`page Example {
|
||||
state page = 1
|
||||
view { <span>{page}</span> }
|
||||
}`),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { NativeCompileError, compileNativeWireFile } from "../src/index.ts";
|
||||
import { NativeCompileError, compileNativeWrnFile } from "../src/index.ts";
|
||||
|
||||
test("compiles portable wrn markup to React Native components", () => {
|
||||
const code = compileNativeWireFile(`page Home {
|
||||
const code = compileNativeWrnFile(`page Home {
|
||||
state count = 0
|
||||
view {
|
||||
<main class="screen">
|
||||
@@ -22,13 +22,13 @@ test("compiles portable wrn markup to React Native components", () => {
|
||||
});
|
||||
|
||||
test("rejects browser-only elements with an actionable error", () => {
|
||||
expect(() => compileNativeWireFile("page Data { view { <table></table> } }")).toThrow(
|
||||
expect(() => compileNativeWrnFile("page Data { view { <table></table> } }")).toThrow(
|
||||
NativeCompileError,
|
||||
);
|
||||
});
|
||||
|
||||
test("compiles loops to native JSX", () => {
|
||||
const code = compileNativeWireFile(
|
||||
const code = compileNativeWrnFile(
|
||||
"page List { view { <ul>{#each items as item, i}<li>{item.name}</li>{:empty}<li>Empty</li>{/each}</ul> } }",
|
||||
);
|
||||
expect(code).toContain("(items).map((item, i)");
|
||||
@@ -36,7 +36,7 @@ test("compiles loops to native JSX", () => {
|
||||
});
|
||||
|
||||
test("selects mobile-only markup and events for native output", () => {
|
||||
const code = compileNativeWireFile(`page Platforms { view {
|
||||
const code = compileNativeWrnFile(`page Platforms { view {
|
||||
<button data-native-only="mobile" @mobile-click="save()" @browser-click="copy()">Save</button>
|
||||
<p data-native-only="browser">Browser help</p>
|
||||
} }`);
|
||||
@@ -47,14 +47,14 @@ test("selects mobile-only markup and events for native output", () => {
|
||||
|
||||
test("rejects declarative Capacitor actions instead of silently dropping them in Expo", () => {
|
||||
expect(() =>
|
||||
compileNativeWireFile(
|
||||
compileNativeWrnFile(
|
||||
`page Share { view { <button data-native-mobile="share">Share</button> } }`,
|
||||
),
|
||||
).toThrow("currently targets browser/Capacitor pages");
|
||||
});
|
||||
|
||||
test("does not emit native visibility directives as React Native props", () => {
|
||||
const code = compileNativeWireFile(
|
||||
const code = compileNativeWrnFile(
|
||||
`page Support { view { <p data-native-requires="camera">Camera</p> } }`,
|
||||
);
|
||||
expect(code).not.toContain("data-native-requires");
|
||||
|
||||
@@ -37,7 +37,7 @@ The `Context` (`ctx`) is the single value passed to middleware and handlers.
|
||||
| `Context` | type | Per-request object: `req`, `url`, `lang`, `t`, `params`, `locals`, `user?`, `ip?`, `cookies`, `session`, `localStorage`. |
|
||||
| `Next` | type | `() => Promise<Response> \| Response` — invokes the next middleware/handler. |
|
||||
| `Middleware` | type | `(ctx, next) => Promise<Response> \| Response`. Return `next()` to continue, or a `Response` to short-circuit. |
|
||||
| `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (wires up cookies, session, localStorage snapshot). |
|
||||
| `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (connects up cookies, session, localStorage snapshot). |
|
||||
| `withContextHeaders(ctx, res)` | fn | Apply accumulated headers (e.g. `Set-Cookie`) from the context onto a response. |
|
||||
| `PageComponent` | type | `(ctx) => string \| Promise<string>` — a page module's default export. |
|
||||
| `PageMeta` / `SeoConfig` | type | `<head>` metadata: `title`, `description`, `canonical`, `robots`, `image`, `twitterCard`, `themeColor`, … |
|
||||
@@ -70,15 +70,15 @@ cookie-backed `SessionStore`.
|
||||
|
||||
### CSRF — `@wrnexus/core`
|
||||
|
||||
Double-submit cookie pattern: a readable `wire-csrf` cookie is echoed in an
|
||||
Double-submit cookie pattern: a readable `wrn-csrf` cookie is echoed in an
|
||||
`x-csrf-token` header on unsafe requests.
|
||||
|
||||
| Export | Signature | Notes |
|
||||
| ----------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `csrfToken(ctx)` | `(Context) => string` | Ensures the CSRF cookie exists and returns its token. |
|
||||
| `verifyCsrf(ctx)` | `(Context) => boolean` | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). |
|
||||
| `csrfProtection()` | `() => Middleware` | 403s unsafe requests with a missing/mismatched token. |
|
||||
| `CSRF_COOKIE` / `CSRF_HEADER` | `"wire-csrf"` / `"x-csrf-token"` | Cookie & header names. |
|
||||
| Export | Signature | Notes |
|
||||
| ----------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `csrfToken(ctx)` | `(Context) => string` | Ensures the CSRF cookie exists and returns its token. |
|
||||
| `verifyCsrf(ctx)` | `(Context) => boolean` | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). |
|
||||
| `csrfProtection()` | `() => Middleware` | 403s unsafe requests with a missing/mismatched token. |
|
||||
| `CSRF_COOKIE` / `CSRF_HEADER` | `"wrn-csrf"` / `"x-csrf-token"` | Cookie & header names. |
|
||||
|
||||
### Rate limiting — `@wrnexus/core`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Context, Middleware } from "./context.ts";
|
||||
|
||||
export const CSRF_COOKIE = "wire-csrf";
|
||||
export const CSRF_COOKIE = "wrn-csrf";
|
||||
export const CSRF_HEADER = "x-csrf-token";
|
||||
|
||||
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
||||
|
||||
@@ -193,7 +193,7 @@ export interface RealtimeEnvelope {
|
||||
}
|
||||
|
||||
/**
|
||||
* A pub/sub bridge for horizontal scaling. Wire the registry to a shared bus
|
||||
* A pub/sub bridge for horizontal scaling. WrNexus the registry to a shared bus
|
||||
* (Redis pub/sub, NATS, …): local broadcasts/`toUser` sends are published to
|
||||
* peers, and messages received from peers are delivered via `registry.deliver`.
|
||||
* Connection-targeted sends (`send`, `to(id)`) stay local (ids are per-process).
|
||||
|
||||
@@ -270,7 +270,7 @@ test("sanitizeFilename strips traversal and separators", () => {
|
||||
});
|
||||
|
||||
test("saveUpload writes a validated file and enforces limits", async () => {
|
||||
const dir = join(tmpdir(), "wire-upload-test");
|
||||
const dir = join(tmpdir(), "wrn-upload-test");
|
||||
const file = new File(["hello upload"], "note.txt", { type: "text/plain" });
|
||||
|
||||
const saved = await saveUpload(file, { dir, allowedTypes: ["text/plain", ".txt"] });
|
||||
|
||||
@@ -111,11 +111,11 @@ Intercepts same-origin `<a>` clicks, fetches the target page, and swaps the `#ap
|
||||
|
||||
Connects to `/realtime/<name>` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes.
|
||||
|
||||
Programmatic API via `window.wire`:
|
||||
Programmatic API via `window.wrn`:
|
||||
|
||||
```ts
|
||||
wire.room(name): Room // open (or reuse) a room connection
|
||||
wire.bindRooms(root?) // (re)bind declarative [data-room] containers
|
||||
wrn.room(name): Room // open (or reuse) a room connection
|
||||
wrn.bindRooms(root?) // (re)bind declarative [data-room] containers
|
||||
|
||||
interface Room {
|
||||
name: string;
|
||||
@@ -211,7 +211,7 @@ A realtime chat, fully declarative:
|
||||
Or drive a room from code:
|
||||
|
||||
```ts
|
||||
const room = wire.room("lobby");
|
||||
const room = wrn.room("lobby");
|
||||
room.on("chat", (msg) => console.log(msg.user, msg.text));
|
||||
room.send({ type: "chat", user: "ada", text: "hi" });
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.15",
|
||||
"version": "0.8.16",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -4,7 +4,7 @@ export const ACTION_RUNTIME = String.raw`
|
||||
window.__wrnexusActionsInstalled = true;
|
||||
|
||||
function csrf() {
|
||||
var match = document.cookie.match(/(?:^|;\s*)wire-csrf=([^;]+)/);
|
||||
var match = document.cookie.match(/(?:^|;\s*)wrn-csrf=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
}
|
||||
|
||||
|
||||
@@ -437,11 +437,11 @@ export const NAV_RUNTIME = String.raw`
|
||||
|
||||
try {
|
||||
if (
|
||||
window.__wireValidate &&
|
||||
typeof window.__wireValidate.init ===
|
||||
window.__wrnValidate &&
|
||||
typeof window.__wrnValidate.init ===
|
||||
"function"
|
||||
) {
|
||||
window.__wireValidate.init(document);
|
||||
window.__wrnValidate.init(document);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -452,11 +452,11 @@ export const NAV_RUNTIME = String.raw`
|
||||
|
||||
try {
|
||||
if (
|
||||
window.wireTheme &&
|
||||
typeof window.wireTheme.bind ===
|
||||
window.wrnTheme &&
|
||||
typeof window.wrnTheme.bind ===
|
||||
"function"
|
||||
) {
|
||||
window.wireTheme.bind(document);
|
||||
window.wrnTheme.bind(document);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
||||
@@ -147,10 +147,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
* identical call in an inline handler works, which is a maddening
|
||||
* inconsistency.
|
||||
*
|
||||
* wireToast is the stable name. toast is the ergonomic alias and defers to
|
||||
* wrnToast is the stable name. toast is the ergonomic alias and defers to
|
||||
* an application that already defines one.
|
||||
*/
|
||||
window.wireToast = toastApi;
|
||||
window.wrnToast = toastApi;
|
||||
if (!window.toast) window.toast = toastApi;
|
||||
|
||||
// Read straight off window, no binding needed (objects, not functions).
|
||||
@@ -229,11 +229,11 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
Array.prototype.forEach.call(rules || [], function (rule) {
|
||||
var cssText = rule.cssText || "";
|
||||
var match;
|
||||
var pattern = /var\(\s*(--wire-[A-Za-z0-9_-]+)/g;
|
||||
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)/g;
|
||||
while ((match = pattern.exec(cssText))) referenced[match[1]] = true;
|
||||
if (rule.style) {
|
||||
Array.prototype.forEach.call(rule.style, function (property) {
|
||||
if (String(property).indexOf("--wire-") === 0) declared[property] = true;
|
||||
if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
|
||||
});
|
||||
}
|
||||
try {
|
||||
@@ -248,7 +248,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll("[style]") || [], function (element) {
|
||||
Array.prototype.forEach.call(element.style || [], function (property) {
|
||||
if (String(property).indexOf("--wire-") === 0) declared[property] = true;
|
||||
if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
|
||||
});
|
||||
});
|
||||
var rendered = window.getComputedStyle(document.documentElement);
|
||||
@@ -2225,7 +2225,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
Array.prototype.slice
|
||||
.call(el.querySelectorAll("[data-for]"))
|
||||
.forEach(function (tpl) {
|
||||
// Only top-level templates here; nested ones are wired by the item
|
||||
// Only top-level templates here; nested ones are connected by the item
|
||||
// that contains them, once it has values to give them.
|
||||
if (tpl.parentNode && tpl.parentNode.closest && tpl.parentNode.closest("[data-for]")) {
|
||||
return;
|
||||
@@ -2289,7 +2289,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
// data-show="expr" — toggle visibility on truthiness. This directive is
|
||||
// intentionally non-destructive because popovers, selects and remote data
|
||||
// controls keep event wiring and state while closed. Use a compiled {#if}
|
||||
// controls keep event integration and state while closed. Use a compiled {#if}
|
||||
// block when the inactive branch must not be rendered.
|
||||
el.querySelectorAll("[data-show]").forEach(function (node) {
|
||||
if (!owns(node)) return;
|
||||
@@ -3367,9 +3367,9 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
nav.dispatchEvent(new CustomEvent("change", { detail: { href: href, label: label } }));
|
||||
}
|
||||
|
||||
function wireScrollspy(nav) {
|
||||
if (nav.__wrnScrollspyWired) return;
|
||||
nav.__wrnScrollspyWired = true;
|
||||
function bindScrollspy(nav) {
|
||||
if (nav.__wrnScrollspyBound) return;
|
||||
nav.__wrnScrollspyBound = true;
|
||||
|
||||
nav.addEventListener("click", function (event) {
|
||||
var t = event.target;
|
||||
@@ -3412,13 +3412,13 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
function setupScrollspy() {
|
||||
if (window.__wrnexusScrollspyBound) return;
|
||||
window.__wrnexusScrollspyBound = true;
|
||||
var wireAll = function () {
|
||||
var bindAll = function () {
|
||||
var navs = document.querySelectorAll("[data-wrn-scrollspy]");
|
||||
for (var index = 0; index < navs.length; index += 1) wireScrollspy(navs[index]);
|
||||
for (var index = 0; index < navs.length; index += 1) bindScrollspy(navs[index]);
|
||||
};
|
||||
wireAll();
|
||||
bindAll();
|
||||
watchDocument(function () {
|
||||
window.setTimeout(wireAll, 0);
|
||||
window.setTimeout(bindAll, 0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3605,7 +3605,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
switcher.addEventListener("toggle", function (event) {
|
||||
var opened = event.target;
|
||||
if (!opened || opened.tagName !== "DETAILS" || !opened.open) return;
|
||||
switcher.querySelectorAll(".wire-preferences__menu[open]").forEach(function (menu) {
|
||||
switcher.querySelectorAll(".wrn-preferences__menu[open]").forEach(function (menu) {
|
||||
if (menu !== opened) menu.removeAttribute("open");
|
||||
});
|
||||
}, true);
|
||||
@@ -3615,14 +3615,14 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
document.addEventListener("pointerdown", function (event) {
|
||||
document.querySelectorAll("[data-wrn-preferences]").forEach(function (switcher) {
|
||||
if (switcher.contains(event.target)) return;
|
||||
switcher.querySelectorAll(".wire-preferences__menu[open]").forEach(function (menu) {
|
||||
switcher.querySelectorAll(".wrn-preferences__menu[open]").forEach(function (menu) {
|
||||
menu.removeAttribute("open");
|
||||
});
|
||||
});
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "Escape") return;
|
||||
document.querySelectorAll("[data-wrn-preferences] .wire-preferences__menu[open]").forEach(function (menu) {
|
||||
document.querySelectorAll("[data-wrn-preferences] .wrn-preferences__menu[open]").forEach(function (menu) {
|
||||
menu.removeAttribute("open");
|
||||
var summary = menu.querySelector(":scope > summary");
|
||||
if (summary) summary.focus();
|
||||
@@ -3648,7 +3648,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var current = window.location.pathname.replace(/\/+$/, "") || "/";
|
||||
var best = null;
|
||||
var bestLength = -1;
|
||||
navbar.querySelectorAll(".wire-navbar__menus a[href]").forEach(function (link) {
|
||||
navbar.querySelectorAll(".wrn-navbar__menus a[href]").forEach(function (link) {
|
||||
var url;
|
||||
try { url = new URL(link.getAttribute("href"), window.location.origin); } catch (_) { return; }
|
||||
if (url.origin !== window.location.origin) return;
|
||||
@@ -3658,12 +3658,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
best = link;
|
||||
bestLength = path.length;
|
||||
});
|
||||
navbar.querySelectorAll(".wire-navbar__menus [aria-current='page']").forEach(function (item) {
|
||||
navbar.querySelectorAll(".wrn-navbar__menus [aria-current='page']").forEach(function (item) {
|
||||
item.removeAttribute("aria-current");
|
||||
});
|
||||
if (best) {
|
||||
best.setAttribute("aria-current", "page");
|
||||
var parentMenu = best.closest(".wire-navbar__dropdown");
|
||||
var parentMenu = best.closest(".wrn-navbar__dropdown");
|
||||
var parentSummary = parentMenu && parentMenu.querySelector(":scope > summary");
|
||||
if (parentSummary) parentSummary.setAttribute("aria-current", "page");
|
||||
}
|
||||
@@ -3675,13 +3675,13 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
typeof window.matchMedia !== "function" ||
|
||||
window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
||||
if (hoverEnabled && hoverCapable) {
|
||||
navbar.querySelectorAll(".wire-navbar__dropdown").forEach(function (menu) {
|
||||
navbar.querySelectorAll(".wrn-navbar__dropdown").forEach(function (menu) {
|
||||
menu.addEventListener("pointerenter", function () {
|
||||
if (menu.__wrnNavbarCloseTimer) {
|
||||
clearTimeout(menu.__wrnNavbarCloseTimer);
|
||||
menu.__wrnNavbarCloseTimer = null;
|
||||
}
|
||||
navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (candidate) {
|
||||
navbar.querySelectorAll(".wrn-navbar__dropdown[open]").forEach(function (candidate) {
|
||||
if (candidate !== menu) candidate.removeAttribute("open");
|
||||
});
|
||||
menu.setAttribute("open", "");
|
||||
@@ -3698,7 +3698,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
navbar.addEventListener("toggle", function (event) {
|
||||
var opened = event.target;
|
||||
if (!opened || opened.tagName !== "DETAILS" || !opened.open) return;
|
||||
navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (menu) {
|
||||
navbar.querySelectorAll(".wrn-navbar__dropdown[open]").forEach(function (menu) {
|
||||
if (menu !== opened) menu.removeAttribute("open");
|
||||
});
|
||||
}, true);
|
||||
@@ -3709,14 +3709,14 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
document.addEventListener("pointerdown", function (event) {
|
||||
document.querySelectorAll("[data-wrn-navbar]").forEach(function (navbar) {
|
||||
if (navbar.contains(event.target)) return;
|
||||
navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (menu) {
|
||||
navbar.querySelectorAll(".wrn-navbar__dropdown[open]").forEach(function (menu) {
|
||||
menu.removeAttribute("open");
|
||||
});
|
||||
});
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "Escape") return;
|
||||
document.querySelectorAll("[data-wrn-navbar] .wire-navbar__dropdown[open]").forEach(function (menu) {
|
||||
document.querySelectorAll("[data-wrn-navbar] .wrn-navbar__dropdown[open]").forEach(function (menu) {
|
||||
menu.removeAttribute("open");
|
||||
var summary = menu.querySelector(":scope > summary");
|
||||
if (summary) summary.focus();
|
||||
@@ -3748,7 +3748,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
function syncComboboxInput(root) {
|
||||
if (!root || !root.hasAttribute("data-wrn-combobox")) return;
|
||||
var input = root.querySelector(".wire-next__combobox-input");
|
||||
var input = root.querySelector(".wrn-next__combobox-input");
|
||||
var api = selectScopeApi(root);
|
||||
if (!input || !api) return;
|
||||
var value = api.call("inputText") || "";
|
||||
@@ -3782,17 +3782,17 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function renderRemoteSelectOptions(root, options) {
|
||||
var list = root.querySelector(".wire-next__select-list");
|
||||
var list = root.querySelector(".wrn-next__select-list");
|
||||
if (!list) return;
|
||||
|
||||
list.querySelectorAll(".wire-next__option-group, .wire-next__select-option, .wire-next__select-message")
|
||||
list.querySelectorAll(".wrn-next__option-group, .wrn-next__select-option, .wrn-next__select-message")
|
||||
.forEach(function (node) { node.remove(); });
|
||||
|
||||
options.forEach(function (option) {
|
||||
if (!option || option.value == null) return;
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "wire-next__select-option";
|
||||
button.className = "wrn-next__select-option";
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("data-option-value", String(option.value));
|
||||
button.disabled = !!option.disabled;
|
||||
@@ -3809,7 +3809,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
button.appendChild(avatar);
|
||||
} else if (option.color) {
|
||||
var dot = document.createElement("i");
|
||||
dot.className = "wire-next__color-dot";
|
||||
dot.className = "wrn-next__color-dot";
|
||||
dot.style.setProperty("--option-color", String(option.color));
|
||||
button.appendChild(dot);
|
||||
}
|
||||
@@ -3826,7 +3826,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
button.appendChild(copy);
|
||||
|
||||
var check = document.createElement("i");
|
||||
check.className = "wire-next__check icon-[lucide--check]";
|
||||
check.className = "wrn-next__check icon-[lucide--check]";
|
||||
check.setAttribute("aria-hidden", "true");
|
||||
check.style.display = "none";
|
||||
button.appendChild(check);
|
||||
@@ -3837,7 +3837,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
api.call("chooseOption", option);
|
||||
window.setTimeout(function () {
|
||||
var selected = api.call("isSelected", option);
|
||||
button.classList.toggle("wire-next__select-option--selected", !!selected);
|
||||
button.classList.toggle("wrn-next__select-option--selected", !!selected);
|
||||
button.setAttribute("aria-selected", selected ? "true" : "false");
|
||||
check.style.display = selected ? "" : "none";
|
||||
syncComboboxInput(root);
|
||||
@@ -3853,8 +3853,8 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
var remote = root.getAttribute("data-remote") === "true";
|
||||
var remoteUrl = root.getAttribute("data-remote-url") || "";
|
||||
var input = root.querySelector(".wire-next__select-search input, .wire-next__combobox-input");
|
||||
var list = root.querySelector(".wire-next__select-list");
|
||||
var input = root.querySelector(".wrn-next__select-search input, .wrn-next__combobox-input");
|
||||
var list = root.querySelector(".wrn-next__select-list");
|
||||
var loadMore = root.querySelector("[data-wrn-select-load-more]");
|
||||
var timer = 0;
|
||||
var requestId = 0;
|
||||
@@ -3968,12 +3968,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
root.addEventListener("click", function (event) {
|
||||
if (!remote || !remoteUrl) return;
|
||||
if (!event.target.closest(".wire-next__select-trigger")) return;
|
||||
if (!event.target.closest(".wrn-next__select-trigger")) return;
|
||||
if (loadedOptions.length === 0) load(false);
|
||||
});
|
||||
root.__wrnexusSelectController.load = load;
|
||||
|
||||
var dropdown = root.querySelector(".wire-next__select-dropdown");
|
||||
var dropdown = root.querySelector(".wrn-next__select-dropdown");
|
||||
var lastOpen = !!(selectScopeApi(root) && selectScopeApi(root).get("open"));
|
||||
function emitOpenStateIfChanged() {
|
||||
var api = selectScopeApi(root);
|
||||
@@ -3998,7 +3998,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
root.addEventListener("click", function (event) {
|
||||
var optionButton = event.target.closest(".wire-next__select-option");
|
||||
var optionButton = event.target.closest(".wrn-next__select-option");
|
||||
if (optionButton) {
|
||||
window.setTimeout(function () {
|
||||
var api = selectScopeApi(root);
|
||||
@@ -4012,7 +4012,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
emitOpenStateIfChanged();
|
||||
}, 0);
|
||||
}
|
||||
if (event.target.closest(".wire-next__clear-select")) {
|
||||
if (event.target.closest(".wrn-next__clear-select")) {
|
||||
window.setTimeout(function () {
|
||||
emitSelectEvent(root, "clear");
|
||||
emitSelectEvent(root, "change", { reason: "clear" });
|
||||
@@ -4064,7 +4064,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
},
|
||||
};
|
||||
root.addEventListener("click", function (event) {
|
||||
if (!event.target.closest(".wire-next__select-option, .wire-next__clear-select")) return;
|
||||
if (!event.target.closest(".wrn-next__select-option, .wrn-next__clear-select")) return;
|
||||
window.setTimeout(function () { syncComboboxInput(root); }, 0);
|
||||
});
|
||||
if (input) {
|
||||
@@ -4093,7 +4093,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (!selectOutsideClickBound) {
|
||||
selectOutsideClickBound = true;
|
||||
document.addEventListener("pointerdown", function (event) {
|
||||
document.querySelectorAll("[data-wrn-select].wire-next--open").forEach(function (select) {
|
||||
document.querySelectorAll("[data-wrn-select].wrn-next--open").forEach(function (select) {
|
||||
if (select.contains(event.target)) return;
|
||||
var api = selectScopeApi(select);
|
||||
if (api) api.set("open", false);
|
||||
@@ -4139,7 +4139,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
function callServerFunction(component, functionName, args) {
|
||||
var csrfMeta = document.querySelector('meta[name="wrnexus-csrf"]');
|
||||
var csrfMatch = /(?:^|;\s*)wire-csrf=([^;]+)/.exec(document.cookie || "");
|
||||
var csrfMatch = /(?:^|;\s*)wrn-csrf=([^;]+)/.exec(document.cookie || "");
|
||||
var csrf = csrfMeta
|
||||
? csrfMeta.getAttribute("content") || ""
|
||||
: csrfMatch
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* `data-room-reset` clear after send). Optional `data-room-user` identifies
|
||||
* the connection.
|
||||
*
|
||||
* 2. Programmatic: `const room = wire.room("chat"); room.on("chat", fn);
|
||||
* 2. Programmatic: `const room = wrn.room("chat"); room.on("chat", fn);
|
||||
* room.send({ type: "chat", text })`. Handles connect, JSON, reconnect.
|
||||
*
|
||||
* Rebinds on `wrnexus:navigated` (client-side nav) and closes rooms whose
|
||||
@@ -21,8 +21,8 @@
|
||||
export const REALTIME_RUNTIME = String.raw`
|
||||
(function () {
|
||||
if (!("WebSocket" in window)) return;
|
||||
var wire = (window.wire = window.wire || {});
|
||||
if (wire.room) return; // already installed
|
||||
var wrn = (window.wrn = window.wrn || {});
|
||||
if (wrn.room) return; // already installed
|
||||
var open = {}; // normalized room+query key -> room connection
|
||||
|
||||
function normalizedQuery(query) {
|
||||
@@ -107,7 +107,7 @@ export const REALTIME_RUNTIME = String.raw`
|
||||
connect();
|
||||
return api;
|
||||
}
|
||||
wire.room = openRoom;
|
||||
wrn.room = openRoom;
|
||||
|
||||
// --- Declarative binding ---------------------------------------------------
|
||||
|
||||
@@ -139,11 +139,11 @@ export const REALTIME_RUNTIME = String.raw`
|
||||
var user = el.getAttribute("data-room-user");
|
||||
var query = el.getAttribute("data-room-query") || (user ? "user=" + encodeURIComponent(user) : "");
|
||||
var key = roomKey(name, query);
|
||||
if (el.__wireRoomBound && el.__wireRoomKey === key) return;
|
||||
el.__wireRoomBound = true;
|
||||
el.__wireRoomKey = key;
|
||||
if (el.__wrnRoomBound && el.__wrnRoomKey === key) return;
|
||||
el.__wrnRoomBound = true;
|
||||
el.__wrnRoomKey = key;
|
||||
var room = openRoom(name, query);
|
||||
el.__wireRoom = room;
|
||||
el.__wrnRoom = room;
|
||||
|
||||
var log = el.querySelector("[data-room-log]");
|
||||
var status = el.querySelector("[data-room-status]");
|
||||
@@ -176,8 +176,8 @@ export const REALTIME_RUNTIME = String.raw`
|
||||
});
|
||||
|
||||
var form = el.querySelector("form[data-room-send]");
|
||||
if (form && !form.__wireRoomForm) {
|
||||
form.__wireRoomForm = true;
|
||||
if (form && !form.__wrnRoomForm) {
|
||||
form.__wrnRoomForm = true;
|
||||
form.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
var data = {};
|
||||
@@ -185,7 +185,7 @@ export const REALTIME_RUNTIME = String.raw`
|
||||
var input = form.elements[i];
|
||||
if (input.name) data[input.name] = input.value;
|
||||
}
|
||||
if (el.__wireRoom) el.__wireRoom.send(data);
|
||||
if (el.__wrnRoom) el.__wrnRoom.send(data);
|
||||
for (var j = 0; j < form.elements.length; j++) {
|
||||
if (form.elements[j].hasAttribute("data-room-reset")) form.elements[j].value = "";
|
||||
}
|
||||
@@ -198,13 +198,13 @@ export const REALTIME_RUNTIME = String.raw`
|
||||
var present = {};
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
bindContainer(containers[i]);
|
||||
if (containers[i].__wireRoomKey) present[containers[i].__wireRoomKey] = true;
|
||||
if (containers[i].__wrnRoomKey) present[containers[i].__wrnRoomKey] = true;
|
||||
}
|
||||
// Close rooms whose container has left the page (client-side navigation).
|
||||
for (var nm in open) if (!present[nm]) open[nm].close();
|
||||
}
|
||||
|
||||
wire.bindRooms = bindAll;
|
||||
wrn.bindRooms = bindAll;
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { bindAll(document); });
|
||||
else bindAll(document);
|
||||
window.addEventListener("wrnexus:navigated", function () { bindAll(document); });
|
||||
|
||||
@@ -17,7 +17,7 @@ export class WrnServerCallError extends Error {
|
||||
|
||||
function csrfFromCookie(): string | undefined {
|
||||
if (typeof document === "undefined") return undefined;
|
||||
const raw = /(?:^|;\s*)wire-csrf=([^;]+)/.exec(document.cookie)?.[1];
|
||||
const raw = /(?:^|;\s*)wrn-csrf=([^;]+)/.exec(document.cookie)?.[1];
|
||||
return raw ? decodeURIComponent(raw) : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ afterEach(() => {
|
||||
|
||||
test("action runtime exposes pending, optimistic, success, and invalidation state", async () => {
|
||||
const win = new Window({ url: "https://example.test/users" });
|
||||
win.document.cookie = "wire-csrf=token";
|
||||
win.document.cookie = "wrn-csrf=token";
|
||||
win.document.body.innerHTML = `<form data-wrn-action="createUser"><input name="name" value="Ada"></form>`;
|
||||
const phases: string[] = [];
|
||||
["optimistic", "pending", "success"].forEach((phase) =>
|
||||
|
||||
@@ -129,14 +129,14 @@ test("exposes programmatic navigation", async () => {
|
||||
test("rebinds theme controls after swapping the page", async () => {
|
||||
install(`<div id="app"><a href="/about" id="lnk">About</a></div>`);
|
||||
let boundRoot: unknown;
|
||||
win.wireTheme = { bind: (root: unknown) => (boundRoot = root) };
|
||||
nextHtml = `<html><body><div id="app"><button data-wire-theme-toggle>Theme</button></div></body></html>`;
|
||||
win.wrnTheme = { bind: (root: unknown) => (boundRoot = root) };
|
||||
nextHtml = `<html><body><div id="app"><button data-wrn-theme-toggle>Theme</button></div></body></html>`;
|
||||
|
||||
win.document.getElementById("lnk").click();
|
||||
await flush();
|
||||
|
||||
expect(boundRoot).toBe(win.document);
|
||||
expect(win.document.querySelector("[data-wire-theme-toggle]")).not.toBeNull();
|
||||
expect(win.document.querySelector("[data-wrn-theme-toggle]")).not.toBeNull();
|
||||
});
|
||||
|
||||
test("unmounts and remounts package runtimes during client navigation", async () => {
|
||||
|
||||
@@ -809,7 +809,7 @@ test("component server calls send the CSRF cookie in the RPC header", async () =
|
||||
);
|
||||
Object.defineProperty(win.document, "cookie", {
|
||||
configurable: true,
|
||||
value: "wire-csrf=rpc%20token",
|
||||
value: "wrn-csrf=rpc%20token",
|
||||
});
|
||||
let request: RequestInit | undefined;
|
||||
const globals = globalThis as Record<string, unknown>;
|
||||
@@ -837,7 +837,7 @@ test("component server calls prefer the rendered CSRF token over stale cookies",
|
||||
);
|
||||
Object.defineProperty(win.document, "cookie", {
|
||||
configurable: true,
|
||||
value: "wire-csrf=stale-token",
|
||||
value: "wrn-csrf=stale-token",
|
||||
});
|
||||
let request: RequestInit | undefined;
|
||||
const globals = globalThis as Record<string, unknown>;
|
||||
@@ -902,8 +902,8 @@ test("development runtime warns when a component binding names a missing functio
|
||||
test("development runtime warns for referenced theme tokens absent from rendered CSS", () => {
|
||||
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||
win.document.head.innerHTML =
|
||||
`<style>:root { --wire-present-test: red; } .probe { ` +
|
||||
`color: var(--wire-present-test); background: var(--wire-missing-test); }</style>`;
|
||||
`<style>:root { --wrn-present-test: red; } .probe { ` +
|
||||
`color: var(--wrn-present-test); background: var(--wrn-missing-test); }</style>`;
|
||||
win.document.body.innerHTML = `<div class="probe" data-scope=""></div>`;
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
@@ -930,15 +930,15 @@ test("development runtime warns for referenced theme tokens absent from rendered
|
||||
}
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(String(warnings[0]?.[0])).toContain("WRN-DEV-THEME-TOKEN-MISSING");
|
||||
expect(String(warnings[0]?.[0])).toContain("--wire-missing-test");
|
||||
expect(String(warnings[0]?.[0])).toContain("--wrn-missing-test");
|
||||
});
|
||||
|
||||
test("development runtime accepts component-local and inline wire variables", () => {
|
||||
test("development runtime accepts component-local and inline wrn variables", () => {
|
||||
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||
win.document.head.innerHTML =
|
||||
`<style>.component { --wire-component-local: red; color: var(--wire-component-local); ` +
|
||||
`background: var(--wire-inline-local); }</style>`;
|
||||
win.document.body.innerHTML = `<div class="component" style="--wire-inline-local: blue" data-scope=""></div>`;
|
||||
`<style>.component { --wrn-component-local: red; color: var(--wrn-component-local); ` +
|
||||
`background: var(--wrn-inline-local); }</style>`;
|
||||
win.document.body.innerHTML = `<div class="component" style="--wrn-inline-local: blue" data-scope=""></div>`;
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
(globalThis as Record<string, unknown>).location = win.location;
|
||||
@@ -1076,9 +1076,9 @@ test("toast() falls back to the console when no toaster is mounted", () => {
|
||||
test("toast is reachable as a real global for compiled client modules", () => {
|
||||
const win = mount(`<div data-scope=""></div>`) as unknown as Window & {
|
||||
toast?: unknown;
|
||||
wireToast?: unknown;
|
||||
wrnToast?: unknown;
|
||||
};
|
||||
expect(typeof win.wireToast).toBe("function");
|
||||
expect(typeof win.wrnToast).toBe("function");
|
||||
expect(typeof win.toast).toBe("function");
|
||||
});
|
||||
|
||||
|
||||
@@ -103,9 +103,9 @@ test("submitting [data-room-send] sends JSON and clears reset fields", () => {
|
||||
expect((inputs[1] as unknown as HTMLInputElement).value).toBe("");
|
||||
});
|
||||
|
||||
test("programmatic wire.room() sends and receives", () => {
|
||||
test("programmatic wrn.room() sends and receives", () => {
|
||||
const win = boot(`<div></div>`) as unknown as Window & {
|
||||
wire: {
|
||||
wrn: {
|
||||
room: (n: string) => {
|
||||
on: (t: string, cb: (m: unknown) => void) => unknown;
|
||||
send: (o: unknown) => void;
|
||||
@@ -113,7 +113,7 @@ test("programmatic wire.room() sends and receives", () => {
|
||||
};
|
||||
};
|
||||
const got: unknown[] = [];
|
||||
const room = win.wire.room("lobby");
|
||||
const room = win.wrn.room("lobby");
|
||||
room.on("ping", (m: unknown) => got.push(m));
|
||||
sockets[0]!.fireOpen();
|
||||
room.send({ type: "hello" });
|
||||
@@ -137,11 +137,11 @@ test("declarative room reconnects when its user identity changes", () => {
|
||||
const win = boot(
|
||||
`<div id="room" data-room="teamspace" data-room-user="asha"></div>`,
|
||||
) as unknown as Window & {
|
||||
wire: { bindRooms: (root?: unknown) => void };
|
||||
wrn: { bindRooms: (root?: unknown) => void };
|
||||
};
|
||||
expect(sockets[0]?.url).toBe("ws://localhost/realtime/teamspace?user=asha");
|
||||
win.document.getElementById("room")!.setAttribute("data-room-user", "neha");
|
||||
win.wire.bindRooms(win.document);
|
||||
win.wrn.bindRooms(win.document);
|
||||
expect(sockets[0]?.readyState).toBe(3);
|
||||
expect(sockets[1]?.url).toBe("ws://localhost/realtime/teamspace?user=neha");
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ const events = await getDb("analytics").all("SELECT * FROM hits");
|
||||
|
||||
Migrations are `.sql` files (in e.g. `app/db/migrations`), each split into
|
||||
`-- +up` and `-- +down` sections. A file with no markers is treated entirely as
|
||||
`up`. Applied names are recorded in a `_wire_migrations` table so each runs once.
|
||||
`up`. Applied names are recorded in a `_wrn_migrations` table so each runs once.
|
||||
|
||||
- `parseMigration(name, content)` → `Migration` (`{ name, up, down }`).
|
||||
- `loadMigrations(dir)` — parse all `.sql` files, sorted by filename.
|
||||
@@ -249,7 +249,7 @@ SQL driver — use `@wrnexus/db/mongo` directly.
|
||||
- **Bun-only.** Uses `bun:sqlite` (SQLite adapter + session store) and `Bun.SQL`
|
||||
(Postgres/MySQL). Migrations/scaffolding use `node:fs`/`node:path`.
|
||||
- Works with `@wrnexus/core` — `sqliteSessionStore` implements its
|
||||
`SessionBackend`; `getDb`/`setDb` are wired by the WrNexus runtime from
|
||||
`SessionBackend`; `getDb`/`setDb` are connected by the WrNexus runtime from
|
||||
`wrnexus.config.ts`.
|
||||
- The `mongodb` npm package is an optional, lazily-imported peer — install it
|
||||
only if you use `@wrnexus/db/mongo`. The core package stays dependency-free.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Migration runner. Migrations are `.sql` files in `app/db/migrations`, each
|
||||
* split into `-- +up` and `-- +down` sections. Applied migrations are recorded
|
||||
* in a `_wire_migrations` table so they run exactly once, newest-last.
|
||||
* in a `_wrn_migrations` table so they run exactly once, newest-last.
|
||||
*
|
||||
* `scaffoldMigration(..., models)` writes an initial migration straight from the
|
||||
* TS models — the source of truth — so you don't hand-write the first schema.
|
||||
@@ -19,8 +19,8 @@ export interface Migration {
|
||||
down: string;
|
||||
}
|
||||
|
||||
const MIGRATIONS_TABLE = "_wire_migrations";
|
||||
const MIGRATION_LOCKS_TABLE = "_wire_migration_locks";
|
||||
const MIGRATIONS_TABLE = "_wrn_migrations";
|
||||
const MIGRATION_LOCKS_TABLE = "_wrn_migration_locks";
|
||||
|
||||
export interface MigrationRunOptions {
|
||||
/** Return pending migration names without executing their SQL. */
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function paginate<T = Row>(
|
||||
const perPage = Math.min(maxPerPage, Math.max(1, Math.floor(opts.perPage ?? 20)));
|
||||
const offset = (page - 1) * perPage;
|
||||
|
||||
const countSql = query.countSql ?? `SELECT COUNT(*) AS n FROM (${query.sql}) AS __wire_sub`;
|
||||
const countSql = query.countSql ?? `SELECT COUNT(*) AS n FROM (${query.sql}) AS __wrn_sub`;
|
||||
const countRow = await db.one<{ n: number | string }>(countSql, params);
|
||||
const total = Number(countRow?.n ?? 0);
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ for (const [label, driver] of [
|
||||
}
|
||||
|
||||
test("migration runner: parse, migrate, status, rollback", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-mig-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-mig-"));
|
||||
writeFileSync(
|
||||
join(dir, "0001_init.sql"),
|
||||
"-- +up\nCREATE TABLE t (id INTEGER PRIMARY KEY, n TEXT);\n-- +down\nDROP TABLE t;",
|
||||
@@ -166,15 +166,15 @@ test("migration lock rejects a concurrent runner and recovers expired locks", as
|
||||
const db = createDb(sqlite());
|
||||
const migrations = [{ name: "0001_lock", up: "CREATE TABLE locked_test (id INTEGER)", down: "" }];
|
||||
await db.exec(
|
||||
"CREATE TABLE _wire_migration_locks (name TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at TEXT NOT NULL)",
|
||||
"CREATE TABLE _wrn_migration_locks (name TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at TEXT NOT NULL)",
|
||||
);
|
||||
await db.exec("INSERT INTO _wire_migration_locks (name, owner, expires_at) VALUES (?, ?, ?)", [
|
||||
await db.exec("INSERT INTO _wrn_migration_locks (name, owner, expires_at) VALUES (?, ?, ?)", [
|
||||
"global",
|
||||
"other",
|
||||
new Date(Date.now() + 60_000).toISOString(),
|
||||
]);
|
||||
await expect(applyMigrations(db, migrations)).rejects.toThrow("WRN-DB-MIGRATION-LOCKED");
|
||||
await db.exec("UPDATE _wire_migration_locks SET expires_at = ?", [new Date(0).toISOString()]);
|
||||
await db.exec("UPDATE _wrn_migration_locks SET expires_at = ?", [new Date(0).toISOString()]);
|
||||
expect(await applyMigrations(db, migrations)).toEqual(["0001_lock"]);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
|
||||
|
||||
## Overview
|
||||
|
||||
This package is the server runtime that powers a WrNexus app in both development and production. A single **request runtime** (`createHandlers`) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about _how_ modules and assets are produced, so the dev and prod entry points wire in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app **gateway** (route several apps by `Host` header behind one port) and a portable `node:http` adapter for WinterCG hosts. It is entirely server-side and Bun-native (`Bun.serve`, `Bun.file`, `Bun.gzipSync`).
|
||||
This package is the server runtime that powers a WrNexus app in both development and production. A single **request runtime** (`createHandlers`) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about _how_ modules and assets are produced, so the dev and prod entry points wrn in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app **gateway** (route several apps by `Host` header behind one port) and a portable `node:http` adapter for WinterCG hosts. It is entirely server-side and Bun-native (`Bun.serve`, `Bun.file`, `Bun.gzipSync`).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -21,16 +21,16 @@ bun add @wrnexus/dev-server
|
||||
|
||||
### Main entry (`@wrnexus/dev-server`)
|
||||
|
||||
| Export | Kind | Purpose |
|
||||
| --------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `startServer(opts: ServeOptions)` | `Promise<RunningServer>` | Start the dev server on `Bun.serve`: builds the router, connects/migrates databases, wires assets + HMR, and starts the file watcher. |
|
||||
| `createHandlers(deps: RuntimeDeps)` | `Handlers` | The shared request runtime (fetch + websocket handlers). Re-exported from `runtime.ts`. |
|
||||
| `createProductionServer(manifest, opts)` | `Bun.Server` | Start the production server from a precompiled manifest. |
|
||||
| `createProductionHandlers(manifest, opts)` | `Handlers` | Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam). |
|
||||
| `startGateway(opts: GatewayOptions)` | `Promise<RunningGateway>` | Boot multiple apps as child processes and route by `Host`. |
|
||||
| `toRequest`, `writeResponse`, `nodeListener`, `serveNode` | functions | `node:http` ↔ WinterCG `Request`/`Response` adapter. |
|
||||
| `RESTART_EXIT_CODE` | `number` (`97`) | Exit code the dev child uses to ask the supervisor for a fresh process. |
|
||||
| `STYLES_HREF`, `HMR_CLIENT_JS` | constants | The global stylesheet URL and the inline HMR client script. |
|
||||
| Export | Kind | Purpose |
|
||||
| --------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `startServer(opts: ServeOptions)` | `Promise<RunningServer>` | Start the dev server on `Bun.serve`: builds the router, connects/migrates databases, connects assets + HMR, and starts the file watcher. |
|
||||
| `createHandlers(deps: RuntimeDeps)` | `Handlers` | The shared request runtime (fetch + websocket handlers). Re-exported from `runtime.ts`. |
|
||||
| `createProductionServer(manifest, opts)` | `Bun.Server` | Start the production server from a precompiled manifest. |
|
||||
| `createProductionHandlers(manifest, opts)` | `Handlers` | Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam). |
|
||||
| `startGateway(opts: GatewayOptions)` | `Promise<RunningGateway>` | Boot multiple apps as child processes and route by `Host`. |
|
||||
| `toRequest`, `writeResponse`, `nodeListener`, `serveNode` | functions | `node:http` ↔ WinterCG `Request`/`Response` adapter. |
|
||||
| `RESTART_EXIT_CODE` | `number` (`97`) | Exit code the dev child uses to ask the supervisor for a fresh process. |
|
||||
| `STYLES_HREF`, `HMR_CLIENT_JS` | constants | The global stylesheet URL and the inline HMR client script. |
|
||||
|
||||
Exported types: `ServeOptions`, `RunningServer`, `RuntimeDeps`, `AssetServer`, `WsData`, `GatewayApp`, `GatewayOptions`, `GatewayAuth`, `GatewaySecurity`, `RunningGateway`, `FetchHandler`.
|
||||
|
||||
@@ -85,7 +85,7 @@ interface RuntimeDeps {
|
||||
getMiddleware(): Promise<Middleware[]>;
|
||||
assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
|
||||
hasStyles?: boolean; // inject the global stylesheet link
|
||||
hasUi?: boolean; // inject the Wire UI stylesheet (/__wrnexus/ui.css)
|
||||
hasUi?: boolean; // inject the WrNexus UI stylesheet (/__wrnexus/ui.css)
|
||||
theme?: ResolvedTheme; // enables /__wrnexus/theme.css + <html data-theme>
|
||||
i18n?: ResolvedI18n; // enables ctx.t, <html lang>, {t:key} markers
|
||||
inlineStyles?: string; // inline small prod stylesheets into <head>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.15",
|
||||
"version": "0.8.16",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -80,7 +80,7 @@ export function createDevAssetServer(
|
||||
pluginAssets: readonly ServedPluginAsset[] = [],
|
||||
): DevAssetServer {
|
||||
let cssCache: string | null = null;
|
||||
let schemasCode = schemasJs ?? "window.__wireSchemas={};";
|
||||
let schemasCode = schemasJs ?? "window.__wrnSchemas={};";
|
||||
|
||||
return {
|
||||
invalidateCss() {
|
||||
|
||||
@@ -567,7 +567,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
if (origin && origin !== url.origin) return false;
|
||||
const cookieHeader = request.headers.get("cookie") ?? "";
|
||||
const cookieToken =
|
||||
/(?:^|;\s*)wire-csrf=([^;]+)/.exec(cookieHeader)?.[1] ??
|
||||
/(?:^|;\s*)wrn-csrf=([^;]+)/.exec(cookieHeader)?.[1] ??
|
||||
/(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(cookieHeader)?.[1];
|
||||
const headerToken =
|
||||
request.headers.get("x-csrf-token") ?? request.headers.get("x-wrnexus-csrf") ?? "";
|
||||
|
||||
@@ -160,7 +160,7 @@ function rewriteArtifactImports(
|
||||
if (entry.resolved.endsWith(".wrn")) {
|
||||
const dependencySource = readFileSync(entry.resolved, "utf8");
|
||||
const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource);
|
||||
const dependency = compileWireArtifacts(
|
||||
const dependency = compileWrnArtifacts(
|
||||
entry.resolved,
|
||||
moduleVersions.get(entry.resolved) ?? 0,
|
||||
);
|
||||
@@ -230,7 +230,7 @@ async function rewriteArtifactImportsAsync(
|
||||
if (replacement.endsWith(".wrn")) {
|
||||
const dependencySource = readFileSync(replacement, "utf8");
|
||||
const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource);
|
||||
const dependency = await compileWireArtifactsAsync(
|
||||
const dependency = await compileWrnArtifactsAsync(
|
||||
replacement,
|
||||
moduleVersions.get(replacement) ?? 0,
|
||||
);
|
||||
@@ -266,7 +266,7 @@ export async function loadModule(file: string): Promise<Record<string, unknown>>
|
||||
const version = moduleVersions.get(file) ?? 0;
|
||||
// `.wrn` files are compiled to TypeScript first, then imported.
|
||||
let target = file.endsWith(".wrn")
|
||||
? (await compileWireArtifactsAsync(file, version)).main
|
||||
? (await compileWrnArtifactsAsync(file, version)).main
|
||||
: file;
|
||||
let temporary = false;
|
||||
// Bun intentionally caches local TS/JS modules by filesystem path and ignores
|
||||
@@ -441,13 +441,13 @@ async function bundleBrowserArtifact(
|
||||
}
|
||||
}
|
||||
|
||||
export function compileWireArtifactsAsync(file: string, version = 0): Promise<WrnCompileArtifacts> {
|
||||
export function compileWrnArtifactsAsync(file: string, version = 0): Promise<WrnCompileArtifacts> {
|
||||
const key = `${file}:${version}`;
|
||||
const active = asyncCompileInProgress.get(key);
|
||||
if (active) return active;
|
||||
const task = (async () => {
|
||||
if (!devCompilerPipeline) {
|
||||
const artifacts = compileWireArtifacts(file, version);
|
||||
const artifacts = compileWrnArtifacts(file, version);
|
||||
const code = readFileSync(artifacts.browser, "utf8");
|
||||
const bundled = await bundleBrowserArtifact(
|
||||
code,
|
||||
@@ -525,7 +525,7 @@ export function resetWrnCompileMetrics(): void {
|
||||
});
|
||||
}
|
||||
|
||||
export function compileWireArtifacts(file: string, version = 0): WrnCompileArtifacts {
|
||||
export function compileWrnArtifacts(file: string, version = 0): WrnCompileArtifacts {
|
||||
const active = compileInProgress.get(file);
|
||||
if (active) return active;
|
||||
const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
|
||||
@@ -598,22 +598,22 @@ export function compileWireArtifacts(file: string, version = 0): WrnCompileArtif
|
||||
|
||||
export async function loadWrnServerModule(file: string): Promise<Record<string, unknown>> {
|
||||
const version = moduleVersions.get(file) ?? 0;
|
||||
const artifact = compileWireArtifacts(file, version).server;
|
||||
const artifact = compileWrnArtifacts(file, version).server;
|
||||
return import(pathToFileURL(artifact).href) as Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export function wrnBrowserArtifact(file: string): string {
|
||||
return compileWireArtifacts(file, moduleVersions.get(file) ?? 0).browser;
|
||||
return compileWrnArtifacts(file, moduleVersions.get(file) ?? 0).browser;
|
||||
}
|
||||
|
||||
export function wrnBrowserArtifactUrl(file: string): string {
|
||||
const artifact = compileWireArtifacts(file, moduleVersions.get(file) ?? 0).browser;
|
||||
const artifact = compileWrnArtifacts(file, moduleVersions.get(file) ?? 0).browser;
|
||||
return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
|
||||
}
|
||||
|
||||
/** Async browser artifact URL used by HMR so imported client modules are bundled before delivery. */
|
||||
export async function wrnBrowserArtifactUrlAsync(file: string): Promise<string> {
|
||||
const artifact = (await compileWireArtifactsAsync(file, moduleVersions.get(file) ?? 0)).browser;
|
||||
const artifact = (await compileWrnArtifactsAsync(file, moduleVersions.get(file) ?? 0)).browser;
|
||||
return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export interface ProdOptions {
|
||||
inlineStyles?: string;
|
||||
/** `stylesPath` contains theme + UI + app CSS in cascade order. */
|
||||
stylesIncludeFramework?: boolean;
|
||||
/** `stylesPath` already contains the shared Wire UI stylesheet. */
|
||||
/** `stylesPath` already contains the shared WrNexus UI stylesheet. */
|
||||
stylesIncludeUi?: boolean;
|
||||
/** Absolute path to the pre-built reactive runtime. */
|
||||
reactivePath?: string;
|
||||
@@ -107,11 +107,11 @@ export interface ProdOptions {
|
||||
themeJsPath?: string;
|
||||
/** Resolved theme config: enables `<html data-theme>` + `theme.css` link. */
|
||||
theme?: ResolvedTheme;
|
||||
/** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
|
||||
/** Absolute path to the pre-built WrNexus UI stylesheet (`ui.css`). */
|
||||
uiCssPath?: string;
|
||||
/** Combined production theme + Wire UI stylesheet. */
|
||||
/** Combined production theme + WrNexus UI stylesheet. */
|
||||
frameworkCssPath?: string;
|
||||
/** Pre-built `window.__wireSchemas = {...}` script for client validation. */
|
||||
/** Pre-built `window.__wrnSchemas = {...}` script for client validation. */
|
||||
schemasJs?: string;
|
||||
/**
|
||||
* Authorization declarations discovered by `wrnexus build` from
|
||||
@@ -367,7 +367,7 @@ function createProdAssetServer(opts: ProdOptions): AssetServer {
|
||||
return (await serveStoredFile(pathname)) ?? new Response("Not Found", { status: 404 });
|
||||
}
|
||||
if (pathname === "/__wrnexus/schemas.js") {
|
||||
return new Response(opts.schemasJs ?? "window.__wireSchemas={};", { headers: JS_HEADERS });
|
||||
return new Response(opts.schemasJs ?? "window.__wrnSchemas={};", { headers: JS_HEADERS });
|
||||
}
|
||||
if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS);
|
||||
const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname);
|
||||
@@ -524,7 +524,7 @@ export function createProductionHandlers(
|
||||
/**
|
||||
* Apply migrations bundled into the build before the server accepts traffic, so
|
||||
* a fresh deploy always runs on the latest schema — exactly like the dev server
|
||||
* auto-migrates on startup. Applied migrations are tracked in `_wire_migrations`,
|
||||
* auto-migrates on startup. Applied migrations are tracked in `_wrn_migrations`,
|
||||
* so this is idempotent and safe to run on every boot. Opt out with
|
||||
* `autoMigrate: false` (e.g. multi-instance deploys that migrate in a release
|
||||
* step). A failed migration is logged but does not crash the server: each
|
||||
|
||||
@@ -4,8 +4,8 @@ import { brotliCompressSync, constants as zlibConstants } from "node:zlib";
|
||||
*
|
||||
* It owns the HTTP/WebSocket dispatch and the SSR document assembly, but knows
|
||||
* nothing about *how* modules or assets are produced — those come in via
|
||||
* `RuntimeDeps`. Dev wires in dynamic module loading + on-the-fly bundling;
|
||||
* prod wires in a static manifest + pre-built chunks on disk.
|
||||
* `RuntimeDeps`. Dev connects in dynamic module loading + on-the-fly bundling;
|
||||
* prod connects in a static manifest + pre-built chunks on disk.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -154,7 +154,7 @@ export interface RuntimeDeps {
|
||||
assets: AssetServer;
|
||||
/** When true, inject the global stylesheet link into every page head. */
|
||||
hasStyles?: boolean;
|
||||
/** When true, inject the Wire UI stylesheet link (`/__wrnexus/ui.css`). */
|
||||
/** When true, inject the WrNexus UI stylesheet link (`/__wrnexus/ui.css`). */
|
||||
hasUi?: boolean;
|
||||
/** Production build combined theme + UI stylesheet. */
|
||||
hasFrameworkStyles?: boolean;
|
||||
@@ -705,10 +705,10 @@ export const HMR_CLIENT_JS = `
|
||||
}
|
||||
|
||||
if (
|
||||
window.wireTheme &&
|
||||
typeof window.wireTheme.bind === "function"
|
||||
window.wrnTheme &&
|
||||
typeof window.wrnTheme.bind === "function"
|
||||
) {
|
||||
window.wireTheme.bind(document);
|
||||
window.wrnTheme.bind(document);
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
@@ -938,7 +938,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
|
||||
// Global <head> additions, identical on every page (styles are global).
|
||||
// Theme tokens load first so global.css and component styles can override them.
|
||||
// Order: theme tokens, then Wire UI, then the app stylesheet — so the app's
|
||||
// Order: theme tokens, then WrNexus UI, then the app stylesheet — so the app's
|
||||
// own CSS (loaded last) can override both the tokens and the UI classes.
|
||||
const headParts: string[] = [];
|
||||
if (!deps.theme && deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
|
||||
|
||||
@@ -21,15 +21,15 @@ export function collectScripts(
|
||||
}
|
||||
if (/\bdata-wrn-action=/.test(body)) scripts.push("/__wrnexus/actions.js");
|
||||
if (
|
||||
/\bdata-wire-theme-(toggle|set)\b/.test(body) ||
|
||||
/\bdata-wire-accent-(set|clear)\b/.test(body)
|
||||
/\bdata-wrn-theme-(toggle|set)\b/.test(body) ||
|
||||
/\bdata-wrn-accent-(set|clear)\b/.test(body)
|
||||
) {
|
||||
scripts.push(THEME_JS_HREF);
|
||||
}
|
||||
if (/\bdata-schema="[A-Za-z0-9_-]+"/.test(body)) {
|
||||
scripts.push("/__wrnexus/schemas.js", "/__wrnexus/validate.js");
|
||||
}
|
||||
if (/\bdata-wire-lang-set\b/.test(body) || /\bselect[^>]*\bdata-wire-lang\b/.test(body)) {
|
||||
if (/\bdata-wrn-lang-set\b/.test(body) || /\bselect[^>]*\bdata-wrn-lang\b/.test(body)) {
|
||||
scripts.push(I18N_JS_HREF);
|
||||
}
|
||||
if (/\bdata-room=/.test(body)) scripts.push("/__wrnexus/realtime.js");
|
||||
|
||||
@@ -54,7 +54,7 @@ test("server actions validate, enforce CSRF, invalidate, and progressively enhan
|
||||
expect(html).toContain('name="wrnexus-csrf"');
|
||||
expect(html).toContain("/__wrnexus/actions.js");
|
||||
const cookie = page!.headers.get("set-cookie")!;
|
||||
const token = /wire-csrf=([^;]+)/.exec(cookie)?.[1];
|
||||
const token = /wrn-csrf=([^;]+)/.exec(cookie)?.[1];
|
||||
if (!token) throw new Error("expected CSRF cookie");
|
||||
|
||||
const invalid = await handlers.fetch(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "nod
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
compileWireArtifactsAsync,
|
||||
compileWrnArtifactsAsync,
|
||||
setCompileCacheDir,
|
||||
setCompileImportOptions,
|
||||
} from "../src/pipeline.ts";
|
||||
@@ -27,7 +27,7 @@ page Settings {
|
||||
|
||||
try {
|
||||
setCompileCacheDir(cache);
|
||||
const artifacts = await compileWireArtifactsAsync(page);
|
||||
const artifacts = await compileWrnArtifactsAsync(page);
|
||||
const browser = readFileSync(artifacts.browser, "utf8");
|
||||
expect(browser).toContain("wrnexus-client-bundled");
|
||||
expect(browser).not.toContain("file://");
|
||||
@@ -65,7 +65,7 @@ page Settings {
|
||||
aliases: { "~": "./app" },
|
||||
autoImport: false,
|
||||
});
|
||||
const artifacts = await compileWireArtifactsAsync(page);
|
||||
const artifacts = await compileWrnArtifactsAsync(page);
|
||||
const browser = readFileSync(artifacts.browser, "utf8");
|
||||
expect(browser).toContain("wrnexus-client-bundled");
|
||||
expect(browser).not.toContain("~/client/storage.ts");
|
||||
@@ -81,7 +81,7 @@ page Settings {
|
||||
aliases: { "~": "./alternate" },
|
||||
autoImport: false,
|
||||
});
|
||||
const changed = await compileWireArtifactsAsync(page);
|
||||
const changed = await compileWrnArtifactsAsync(page);
|
||||
expect(changed.browser).not.toBe(artifacts.browser);
|
||||
} finally {
|
||||
setCompileCacheDir(null);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import {
|
||||
compileWireArtifacts,
|
||||
compileWrnArtifacts,
|
||||
setCompileCacheDir,
|
||||
setCompileImportOptions,
|
||||
} from "../src/pipeline.ts";
|
||||
@@ -31,24 +31,24 @@ function fixture(): { root: string; page: string } {
|
||||
test("legacy, compatible, and explicit import modes are enforced from app config", () => {
|
||||
const { root, page } = fixture();
|
||||
setCompileImportOptions(root, { mode: "legacy" });
|
||||
expect(() => compileWireArtifacts(page, 1)).not.toThrow();
|
||||
expect(() => compileWrnArtifacts(page, 1)).not.toThrow();
|
||||
|
||||
const warnings: string[] = [];
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args) => warnings.push(args.join(" "));
|
||||
try {
|
||||
setCompileImportOptions(root, { mode: "compatible" });
|
||||
expect(() => compileWireArtifacts(page, 2)).not.toThrow();
|
||||
expect(() => compileWrnArtifacts(page, 2)).not.toThrow();
|
||||
expect(warnings.some((message) => message.includes("WRN-IMPORT-IMPLICIT"))).toBe(true);
|
||||
|
||||
setCompileImportOptions(root, { mode: "explicit" });
|
||||
expect(() => compileWireArtifacts(page, 3)).toThrow("WRN-IMPORT-IMPLICIT");
|
||||
expect(() => compileWrnArtifacts(page, 3)).toThrow("WRN-IMPORT-IMPLICIT");
|
||||
|
||||
writeFileSync(
|
||||
page,
|
||||
'import Card from "@/components/Card.wrn"\npage Home { view { <Card /> } }',
|
||||
);
|
||||
expect(() => compileWireArtifacts(page, 4)).not.toThrow();
|
||||
expect(() => compileWrnArtifacts(page, 4)).not.toThrow();
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
@@ -68,5 +68,5 @@ test("compiler-native reactive elements do not require application imports", ()
|
||||
}`,
|
||||
);
|
||||
setCompileImportOptions(root, { mode: "explicit" });
|
||||
expect(() => compileWireArtifacts(page, 5)).not.toThrow();
|
||||
expect(() => compileWrnArtifacts(page, 5)).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
compileWireArtifacts,
|
||||
compileWireArtifactsAsync,
|
||||
compileWrnArtifacts,
|
||||
compileWrnArtifactsAsync,
|
||||
getWrnCompileMetrics,
|
||||
invalidateModule,
|
||||
loadModule,
|
||||
@@ -32,7 +32,7 @@ test("development compilation awaits plugin AST and code transforms", async () =
|
||||
});
|
||||
try {
|
||||
writeFileSync(file, "page Home { view { <h1>Plugin</h1> } }\n");
|
||||
const artifact = await compileWireArtifactsAsync(file);
|
||||
const artifact = await compileWrnArtifactsAsync(file);
|
||||
expect(astTransformed).toBeTrue();
|
||||
expect((await import(artifact.main)).pluginTransformed).toBeTrue();
|
||||
} finally {
|
||||
@@ -48,10 +48,10 @@ test("WRN compilation exposes cache hit, miss, timing, and error metrics", () =>
|
||||
resetWrnCompileMetrics();
|
||||
try {
|
||||
writeFileSync(file, "page Home { view { <h1>Metrics</h1> } }\n");
|
||||
compileWireArtifacts(file);
|
||||
compileWireArtifacts(file);
|
||||
compileWrnArtifacts(file);
|
||||
compileWrnArtifacts(file);
|
||||
writeFileSync(file, "page Broken { view { <h1> } }\n");
|
||||
expect(() => compileWireArtifacts(file)).toThrow();
|
||||
expect(() => compileWrnArtifacts(file)).toThrow();
|
||||
expect(getWrnCompileMetrics()).toMatchObject({
|
||||
hits: 1,
|
||||
misses: 2,
|
||||
|
||||
@@ -2,13 +2,13 @@ import { expect, test } from "bun:test";
|
||||
import { HMR_CLIENT_JS, collectScripts } from "../src/runtime.ts";
|
||||
|
||||
test("accent controls load the shared theme runtime", () => {
|
||||
expect(collectScripts('<button data-wire-accent-set="rose">Rose</button>')).toContain(
|
||||
expect(collectScripts('<button data-wrn-accent-set="rose">Rose</button>')).toContain(
|
||||
"/__wrnexus/theme.js",
|
||||
);
|
||||
});
|
||||
|
||||
test("accent clear controls load the shared theme runtime", () => {
|
||||
expect(collectScripts("<button data-wire-accent-clear>Reset</button>")).toContain(
|
||||
expect(collectScripts("<button data-wrn-accent-clear>Reset</button>")).toContain(
|
||||
"/__wrnexus/theme.js",
|
||||
);
|
||||
});
|
||||
@@ -20,9 +20,9 @@ test("HMR preserves the active browser theme and accent", () => {
|
||||
|
||||
expect(HMR_CLIENT_JS).toMatch(/var nextLang\s*=\s*doc\.documentElement\.getAttribute\("lang"\)/);
|
||||
|
||||
expect(HMR_CLIENT_JS).toMatch(/typeof\s+window\.wireTheme\.bind\s*===\s*"function"/);
|
||||
expect(HMR_CLIENT_JS).toMatch(/typeof\s+window\.wrnTheme\.bind\s*===\s*"function"/);
|
||||
|
||||
expect(HMR_CLIENT_JS).toMatch(/window\.wireTheme\.bind\(document\)/);
|
||||
expect(HMR_CLIENT_JS).toMatch(/window\.wrnTheme\.bind\(document\)/);
|
||||
|
||||
expect(HMR_CLIENT_JS).toContain('new CustomEvent("wrnexus:hmr-updated"');
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test } from "bun:test";
|
||||
import { createDevAssetServer } from "../src/assets.ts";
|
||||
|
||||
test("UI CSS is read from its live source after an HMR change", async () => {
|
||||
let css = ".wire-card { color: red; }";
|
||||
let css = ".wrn-card { color: red; }";
|
||||
const assets = createDevAssetServer(
|
||||
process.cwd(),
|
||||
"development",
|
||||
@@ -14,7 +14,7 @@ test("UI CSS is read from its live source after an HMR change", async () => {
|
||||
const first = await assets.serve("/__wrnexus/ui.css");
|
||||
expect(await first?.text()).toContain("color: red");
|
||||
|
||||
css = ".wire-card { color: blue; }";
|
||||
css = ".wrn-card { color: blue; }";
|
||||
assets.invalidateCss();
|
||||
|
||||
const second = await assets.serve("/__wrnexus/ui.css");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -36,7 +36,7 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => {
|
||||
const count=document.querySelectorAll("*").length;if(count>1500)found.push(issue("html/dom-size","html",count>3000?"error":"warning","DOM is large","The page contains "+count+" elements.",null,"Reduce wrappers and render hidden content on demand."));
|
||||
q("button button,button a,a a,a button").forEach(el=>found.push(issue("html/nested-interactive","html","error","Interactive elements are nested","Nested links or buttons create invalid interaction behavior.",el,"Use one interactive element.")));
|
||||
q("a").forEach(el=>{const href=el.getAttribute("href");if(!href||href==="#")found.push(issue("links/empty","links","warning","Link has no useful destination","The href is missing or only #.",el,"Use a real URL or a button."));if(el.target==="_blank"&&!/\b(noopener|noreferrer)\b/.test(el.rel))found.push(issue("links/blank-rel","security","warning","New-tab link lacks rel protection","The opened page may access window.opener.",el,"Add rel=\"noopener noreferrer\"."));});
|
||||
q("form").forEach(el=>{const method=(el.getAttribute("method")||"get").toLowerCase();const action=el.getAttribute("action")||"";if(location.protocol==="https:"&&action.startsWith("http://"))found.push(issue("security/insecure-form-action","security","error","Form submits over HTTP",action,el,"Use an HTTPS or same-origin form action."));if(method!=="get"&&!el.hasAttribute("data-csrf")&&!el.querySelector('input[name="wire-csrf"],input[name="_csrf"]'))found.push(issue("security/missing-csrf","security","error","State-changing form has no CSRF marker","Cookie-authenticated form submissions require CSRF protection.",el,"Add data-csrf or a framework-generated CSRF field."));});
|
||||
q("form").forEach(el=>{const method=(el.getAttribute("method")||"get").toLowerCase();const action=el.getAttribute("action")||"";if(location.protocol==="https:"&&action.startsWith("http://"))found.push(issue("security/insecure-form-action","security","error","Form submits over HTTP",action,el,"Use an HTTPS or same-origin form action."));if(method!=="get"&&!el.hasAttribute("data-csrf")&&!el.querySelector('input[name="wrn-csrf"],input[name="_csrf"]'))found.push(issue("security/missing-csrf","security","error","State-changing form has no CSRF marker","Cookie-authenticated form submissions require CSRF protection.",el,"Add data-csrf or a framework-generated CSRF field."));});
|
||||
q("script:not([src]):not([type='application/json']):not([nonce])").forEach(el=>found.push(issue("security/inline-script","security","warning","Inline script has no CSP nonce","A strict Content Security Policy will block this script.",el,"Move code to a client module or attach the request nonce.")));
|
||||
q("iframe:not([sandbox])").forEach(el=>found.push(issue("security/iframe-sandbox","security","warning","Iframe is not sandboxed","Untrusted iframe content has broad browser capabilities.",el,"Add the narrowest sandbox and permissions policy.")));
|
||||
q("[src],[href],[action]").forEach(el=>{const raw=el.getAttribute("src")||el.getAttribute("href")||el.getAttribute("action")||"";if(location.protocol==="https:"&&raw.startsWith("http://"))found.push(issue("security/mixed-content","security","error","Mixed-content resource",raw,el,"Use HTTPS or a same-origin resource."));});
|
||||
|
||||
@@ -45,7 +45,7 @@ export const securityRules: DevToolbarRule[] = [
|
||||
const method = (form.method || "get").toUpperCase();
|
||||
if (
|
||||
!["GET", "HEAD"].includes(method) &&
|
||||
!form.querySelector('input[name="wire-csrf"], input[name="_csrf"]')
|
||||
!form.querySelector('input[name="wrn-csrf"], input[name="_csrf"]')
|
||||
) {
|
||||
issues.push(
|
||||
createIssue({
|
||||
|
||||
@@ -22,7 +22,7 @@ const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
|
||||
default: "en",
|
||||
locales: ["en", "mr", "hi"],
|
||||
fallbacks: { "mr-IN": ["mr", "en"] },
|
||||
cookie: { name: "wire-lang", sameSite: "Lax", secure: true },
|
||||
cookie: { name: "wrn-lang", sameSite: "Lax", secure: true },
|
||||
});
|
||||
|
||||
const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
|
||||
@@ -57,7 +57,7 @@ Enable `i18nPlugin()` to use:
|
||||
- `<LanguageSwitcher />`
|
||||
- `<LocaleStatus />`
|
||||
|
||||
`LanguageSwitcher` renders a native `select[data-wire-lang]`. The packaged runtime validates the
|
||||
`LanguageSwitcher` renders a native `select[data-wrn-lang]`. The packaged runtime validates the
|
||||
selection against the configured locales, writes the configured language cookie, updates the
|
||||
document `lang`/`dir` attributes, emits `wrnexus:language-change`, and reloads so the next SSR
|
||||
request uses the same cookie. No application-owned browser script is required.
|
||||
|
||||
@@ -30,76 +30,76 @@ component LanguageSwitcher {
|
||||
view {
|
||||
<div
|
||||
{...attrs}
|
||||
class='wire-language-switcher {fullWidth ? "wire-language-switcher--full" : ""} {responsive ? "wire-language-switcher--responsive" : ""} {compact || variant == "compact" ? "wire-language-switcher--compact" : ""} {class}'
|
||||
class='wrn-language-switcher {fullWidth ? "wrn-language-switcher--full" : ""} {responsive ? "wrn-language-switcher--responsive" : ""} {compact || variant == "compact" ? "wrn-language-switcher--compact" : ""} {class}'
|
||||
data-color='{color}'
|
||||
data-size='{size}'
|
||||
data-variant='{variant}'
|
||||
data-current-language='{current}'
|
||||
>
|
||||
{#if variant == "segmented"}
|
||||
<fieldset class="wire-language-switcher__fieldset">
|
||||
{#if showLabel}<legend class="wire-language-switcher__label">{label}</legend>{/if}
|
||||
<div class="wire-language-switcher__segments" role="group" aria-label='{label}'>
|
||||
<fieldset class="wrn-language-switcher__fieldset">
|
||||
{#if showLabel}<legend class="wrn-language-switcher__label">{label}</legend>{/if}
|
||||
<div class="wrn-language-switcher__segments" role="group" aria-label='{label}'>
|
||||
{#each locales as locale}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-language-switcher__segment"
|
||||
data-wire-lang-set='{locale.value}'
|
||||
class="wrn-language-switcher__segment"
|
||||
data-wrn-lang-set='{locale.value}'
|
||||
aria-pressed='{locale.value == current}'
|
||||
disabled='{locale.disabled}'
|
||||
@click='selected(locale.value, event)'
|
||||
><span class="wire-language-switcher__long-label">{locale.label}</span><span class="wire-language-switcher__short-label">{locale.shortLabel}</span></button>
|
||||
><span class="wrn-language-switcher__long-label">{locale.label}</span><span class="wrn-language-switcher__short-label">{locale.shortLabel}</span></button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if showHelper && helperText}<span class="wire-language-switcher__helper">{helperText}</span>{/if}
|
||||
{#if showHelper && helperText}<span class="wrn-language-switcher__helper">{helperText}</span>{/if}
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if variant != "segmented"}
|
||||
<label class="wire-language-switcher__field">
|
||||
{#if showLabel}<span class="wire-language-switcher__label">{label}</span>{/if}
|
||||
<span class="wire-language-switcher__control">
|
||||
{#if icon}<span class='{icon} wire-language-switcher__icon' aria-hidden="true"></span>{/if}
|
||||
<label class="wrn-language-switcher__field">
|
||||
{#if showLabel}<span class="wrn-language-switcher__label">{label}</span>{/if}
|
||||
<span class="wrn-language-switcher__control">
|
||||
{#if icon}<span class='{icon} wrn-language-switcher__icon' aria-hidden="true"></span>{/if}
|
||||
<select
|
||||
data-wire-lang
|
||||
data-wrn-lang
|
||||
name="language"
|
||||
aria-label='{label}'
|
||||
class="wire-language-switcher__select"
|
||||
class="wrn-language-switcher__select"
|
||||
@change='changed(event)'
|
||||
>
|
||||
{#if placeholder}<option value="" disabled selected='{current == ""}'>{placeholder}</option>{/if}
|
||||
{#each locales as locale}<option value='{locale.value}' selected='{locale.value == current}' disabled='{locale.disabled}' data-wire-lang-option data-label-long='{locale.label}' data-label-short='{locale.shortLabel}'>{locale.label}</option>{/each}
|
||||
{#each locales as locale}<option value='{locale.value}' selected='{locale.value == current}' disabled='{locale.disabled}' data-wrn-lang-option data-label-long='{locale.label}' data-label-short='{locale.shortLabel}'>{locale.label}</option>{/each}
|
||||
</select>
|
||||
<span class="icon-[lucide--chevron-down] wire-language-switcher__chevron" aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--chevron-down] wrn-language-switcher__chevron" aria-hidden="true"></span>
|
||||
</span>
|
||||
{#if showHelper && helperText}<span class="wire-language-switcher__helper">{helperText}</span>{/if}
|
||||
{#if showHelper && helperText}<span class="wrn-language-switcher__helper">{helperText}</span>{/if}
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-language-switcher { min-width: 13rem; color: var(--wire-color-text); }
|
||||
.wire-language-switcher--full, .wire-language-switcher--full .wire-language-switcher__field { width: 100%; }
|
||||
.wire-language-switcher__field, .wire-language-switcher__fieldset { display: flex; margin: 0; padding: 0; border: 0; flex-direction: column; gap: .4rem; }
|
||||
.wire-language-switcher__label { padding: 0; font-size: .8125rem; line-height: 1.2; font-weight: 650; color: var(--wire-color-text); }
|
||||
.wire-language-switcher__control { position: relative; display: flex; align-items: center; }
|
||||
.wire-language-switcher__select { width: 100%; min-height: 2.75rem; appearance: none; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius-lg); background: var(--wire-color-surface); padding: .65rem 2.5rem .65rem 2.35rem; color: var(--wire-color-text); font: inherit; font-size: .875rem; font-weight: 550; box-shadow: 0 1px 2px color-mix(in srgb, var(--wire-color-text) 7%, transparent); outline: none; transition: border-color .16s ease, box-shadow .16s ease, background .16s ease; }
|
||||
.wire-language-switcher__select:hover { border-color: color-mix(in srgb, var(--wire-color-primary) 45%, var(--wire-color-border)); }
|
||||
.wire-language-switcher__select:focus-visible { border-color: var(--wire-color-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-color-primary) 18%, transparent); }
|
||||
.wire-language-switcher__icon, .wire-language-switcher__chevron { position: absolute; z-index: 1; width: 1rem; height: 1rem; pointer-events: none; color: var(--wire-color-muted); }
|
||||
.wire-language-switcher__icon { left: .8rem; }
|
||||
.wire-language-switcher__chevron { right: .8rem; }
|
||||
.wire-language-switcher__helper { font-size: .72rem; line-height: 1.35; color: var(--wire-color-muted); }
|
||||
.wire-language-switcher--compact { min-width: 0; }
|
||||
.wire-language-switcher--compact .wire-language-switcher__label, .wire-language-switcher--compact .wire-language-switcher__helper { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
|
||||
.wire-language-switcher--compact .wire-language-switcher__select { min-height: 2.25rem; border-radius: 999px; padding-top: .4rem; padding-bottom: .4rem; }
|
||||
.wire-language-switcher__segments { display: inline-flex; flex-wrap: wrap; gap: .25rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius-xl); background: var(--wire-color-surface-2); padding: .25rem; }
|
||||
.wire-language-switcher__segment { min-height: 2.25rem; border: 0; border-radius: var(--wire-radius-lg); background: transparent; padding: .45rem .8rem; color: var(--wire-color-muted); font: inherit; font-size: .8125rem; font-weight: 650; cursor: pointer; transition: color .16s ease, background .16s ease, box-shadow .16s ease; }
|
||||
.wire-language-switcher__segment:hover { color: var(--wire-color-text); }
|
||||
.wire-language-switcher__segment[aria-pressed="true"] { background: var(--wire-color-surface); color: var(--wire-color-primary); box-shadow: 0 1px 3px color-mix(in srgb, var(--wire-color-text) 12%, transparent); }
|
||||
.wire-language-switcher__segment:focus-visible { outline: 2px solid var(--wire-color-primary); outline-offset: 2px; }
|
||||
.wire-language-switcher__short-label { display: none; }
|
||||
@media (max-width: 640px) { .wire-language-switcher--responsive { width: 100%; min-width: 0; } .wire-language-switcher--responsive .wire-language-switcher__segments { display: grid; width: 100%; grid-template-columns: repeat(auto-fit, minmax(3.5rem, 1fr)); } .wire-language-switcher--responsive .wire-language-switcher__long-label { display: none; } .wire-language-switcher--responsive .wire-language-switcher__short-label { display: inline; } }
|
||||
@media (prefers-reduced-motion: reduce) { .wire-language-switcher__select, .wire-language-switcher__segment { transition: none; } }
|
||||
.wrn-language-switcher { min-width: 13rem; color: var(--wrn-color-text); }
|
||||
.wrn-language-switcher--full, .wrn-language-switcher--full .wrn-language-switcher__field { width: 100%; }
|
||||
.wrn-language-switcher__field, .wrn-language-switcher__fieldset { display: flex; margin: 0; padding: 0; border: 0; flex-direction: column; gap: .4rem; }
|
||||
.wrn-language-switcher__label { padding: 0; font-size: .8125rem; line-height: 1.2; font-weight: 650; color: var(--wrn-color-text); }
|
||||
.wrn-language-switcher__control { position: relative; display: flex; align-items: center; }
|
||||
.wrn-language-switcher__select { width: 100%; min-height: 2.75rem; appearance: none; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface); padding: .65rem 2.5rem .65rem 2.35rem; color: var(--wrn-color-text); font: inherit; font-size: .875rem; font-weight: 550; box-shadow: 0 1px 2px color-mix(in srgb, var(--wrn-color-text) 7%, transparent); outline: none; transition: border-color .16s ease, box-shadow .16s ease, background .16s ease; }
|
||||
.wrn-language-switcher__select:hover { border-color: color-mix(in srgb, var(--wrn-color-primary) 45%, var(--wrn-color-border)); }
|
||||
.wrn-language-switcher__select:focus-visible { border-color: var(--wrn-color-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 18%, transparent); }
|
||||
.wrn-language-switcher__icon, .wrn-language-switcher__chevron { position: absolute; z-index: 1; width: 1rem; height: 1rem; pointer-events: none; color: var(--wrn-color-muted); }
|
||||
.wrn-language-switcher__icon { left: .8rem; }
|
||||
.wrn-language-switcher__chevron { right: .8rem; }
|
||||
.wrn-language-switcher__helper { font-size: .72rem; line-height: 1.35; color: var(--wrn-color-muted); }
|
||||
.wrn-language-switcher--compact { min-width: 0; }
|
||||
.wrn-language-switcher--compact .wrn-language-switcher__label, .wrn-language-switcher--compact .wrn-language-switcher__helper { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
|
||||
.wrn-language-switcher--compact .wrn-language-switcher__select { min-height: 2.25rem; border-radius: 999px; padding-top: .4rem; padding-bottom: .4rem; }
|
||||
.wrn-language-switcher__segments { display: inline-flex; flex-wrap: wrap; gap: .25rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-xl); background: var(--wrn-color-surface-2); padding: .25rem; }
|
||||
.wrn-language-switcher__segment { min-height: 2.25rem; border: 0; border-radius: var(--wrn-radius-lg); background: transparent; padding: .45rem .8rem; color: var(--wrn-color-muted); font: inherit; font-size: .8125rem; font-weight: 650; cursor: pointer; transition: color .16s ease, background .16s ease, box-shadow .16s ease; }
|
||||
.wrn-language-switcher__segment:hover { color: var(--wrn-color-text); }
|
||||
.wrn-language-switcher__segment[aria-pressed="true"] { background: var(--wrn-color-surface); color: var(--wrn-color-primary); box-shadow: 0 1px 3px color-mix(in srgb, var(--wrn-color-text) 12%, transparent); }
|
||||
.wrn-language-switcher__segment:focus-visible { outline: 2px solid var(--wrn-color-primary); outline-offset: 2px; }
|
||||
.wrn-language-switcher__short-label { display: none; }
|
||||
@media (max-width: 640px) { .wrn-language-switcher--responsive { width: 100%; min-width: 0; } .wrn-language-switcher--responsive .wrn-language-switcher__segments { display: grid; width: 100%; grid-template-columns: repeat(auto-fit, minmax(3.5rem, 1fr)); } .wrn-language-switcher--responsive .wrn-language-switcher__long-label { display: none; } .wrn-language-switcher--responsive .wrn-language-switcher__short-label { display: inline; } }
|
||||
@media (prefers-reduced-motion: reduce) { .wrn-language-switcher__select, .wrn-language-switcher__segment { transition: none; } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ component LocaleStatus {
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge label='{locale}' color='{color}' variant="soft" size='{size}' />
|
||||
<Badge label='{direction == "rtl" ? "RTL" : "LTR"}' color="neutral" variant="outline" size='{size}' />
|
||||
{#if total > 0}<span class="text-sm text-[var(--wire-color-muted)]">{translated}/{total} translations</span>{/if}
|
||||
{#if total > 0}<span class="text-sm text-[var(--wrn-color-muted)]">{translated}/{total} translations</span>{/if}
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user