chore(release): publish db 0.8.21, cli 0.8.65 and seven in-flight packages
Carries the rollback refusal in db and the matching CLI message, together with the auth, captcha, csr, dev-server, dev-toolbar, ui and validation changes that were already in the working tree, since publishing stages from it and those files would otherwise ship without being recorded here. @wrnexus/auth 0.8.28 -> 0.8.29 @wrnexus/captcha 0.8.15 -> 0.8.16 @wrnexus/cli 0.8.64 -> 0.8.65 @wrnexus/csr 0.8.37 -> 0.8.38 @wrnexus/db 0.8.20 -> 0.8.21 @wrnexus/dev-server 0.8.60 -> 0.8.61 @wrnexus/dev-toolbar 0.8.17 -> 0.8.18 @wrnexus/ui 0.8.75 -> 0.8.76 @wrnexus/validation 0.8.12 -> 0.8.13 Also declares Event among the browser globals captcha's client asset already lists, which was the last lint failure, and reformats four files. typecheck, lint, 1998 tests and format:check all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b807f7b5a7
commit
06433e4ac4
28 files changed
+194
-43
No files matched your search
@@ -235,7 +235,7 @@
|
|||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.26",
|
"version": "0.8.29",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
},
|
},
|
||||||
"packages/captcha": {
|
"packages/captcha": {
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.8.14",
|
"version": "0.8.16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.64",
|
"version": "0.8.65",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts",
|
"wrnexus": "src/index.ts",
|
||||||
},
|
},
|
||||||
@@ -342,14 +342,14 @@
|
|||||||
},
|
},
|
||||||
"packages/csr": {
|
"packages/csr": {
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.35",
|
"version": "0.8.38",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/db": {
|
"packages/db": {
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.20",
|
"version": "0.8.21",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -357,7 +357,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.59",
|
"version": "0.8.61",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/cache": "workspace:*",
|
"@wrnexus/cache": "workspace:*",
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-toolbar": {
|
"packages/dev-toolbar": {
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.8.16",
|
"version": "0.8.18",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -688,7 +688,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.70",
|
"version": "0.8.76",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -709,7 +709,7 @@
|
|||||||
},
|
},
|
||||||
"packages/validation": {
|
"packages/validation": {
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.8.11",
|
"version": "0.8.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ component InvitationAccept {
|
|||||||
token = ""
|
token = ""
|
||||||
redirect = "/account"
|
redirect = "/account"
|
||||||
organization = "your workspace"
|
organization = "your workspace"
|
||||||
|
workspace = ""
|
||||||
|
team = ""
|
||||||
|
role = ""
|
||||||
|
expires = ""
|
||||||
inviter = "A workspace administrator"
|
inviter = "A workspace administrator"
|
||||||
title = "Accept your invitation"
|
title = "Accept your invitation"
|
||||||
description = "Create your password to join the workspace."
|
description = "Create your password to join the workspace."
|
||||||
@@ -18,10 +22,17 @@ component InvitationAccept {
|
|||||||
}
|
}
|
||||||
view {
|
view {
|
||||||
<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}'>
|
<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>
|
<span class="mx-auto 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>
|
<h1 class="mb-0 mt-4 text-2xl font-semibold">{title}</h1>
|
||||||
<p class="mb-0 mt-2 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
<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>
|
<div class="mt-5 grid gap-2 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>
|
||||||
|
{#if workspace}<p class="m-0 text-[var(--wrn-color-muted)]"><span class="font-medium text-[var(--wrn-color-text)]">Workspace:</span> {workspace}</p>{/if}
|
||||||
|
{#if team}<p class="m-0 text-[var(--wrn-color-muted)]"><span class="font-medium text-[var(--wrn-color-text)]">Team:</span> {team}</p>{/if}
|
||||||
|
{#if role}<p class="m-0 text-[var(--wrn-color-muted)]"><span class="font-medium text-[var(--wrn-color-text)]">Role:</span> {role}</p>{/if}
|
||||||
|
<p class="m-0 text-[var(--wrn-color-muted)]"><span class="font-medium text-[var(--wrn-color-text)]">Invited by:</span> {inviter}</p>
|
||||||
|
{#if expires}<p class="m-0 text-xs text-[var(--wrn-color-muted)]">Invitation expires {expires}</p>{/if}
|
||||||
|
</div>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-5 space-y-4">
|
<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 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}" />
|
<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}" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.26",
|
"version": "0.8.29",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ export interface AuthEngine {
|
|||||||
ttlMs?: number;
|
ttlMs?: number;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
}): Promise<{ token: string; user: ReturnType<typeof publicUser> }>;
|
}): Promise<{ token: string; user: ReturnType<typeof publicUser> }>;
|
||||||
|
inspectInvitation(
|
||||||
|
token: string,
|
||||||
|
): Promise<{ target?: string; expiresAt: number; metadata: Record<string, unknown> } | undefined>;
|
||||||
acceptInvitation(
|
acceptInvitation(
|
||||||
token: string,
|
token: string,
|
||||||
input: { password?: string; displayName?: string },
|
input: { password?: string; displayName?: string },
|
||||||
@@ -1483,6 +1486,16 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
return { token, user: publicUser(user) };
|
return { token, user: publicUser(user) };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async inspectInvitation(raw) {
|
||||||
|
const token = await inspectToken(raw, "invite");
|
||||||
|
if (!token) return undefined;
|
||||||
|
return {
|
||||||
|
target: token.target,
|
||||||
|
expiresAt: token.expiresAt,
|
||||||
|
metadata: { ...(token.metadata ?? {}) },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async acceptInvitation(raw, input) {
|
async acceptInvitation(raw, input) {
|
||||||
const token = await inspectToken(raw, "invite");
|
const token = await inspectToken(raw, "invite");
|
||||||
if (!token)
|
if (!token)
|
||||||
|
|||||||
@@ -388,9 +388,14 @@ describe("authentication engine", () => {
|
|||||||
displayName: "Invited User",
|
displayName: "Invited User",
|
||||||
roles: ["member"],
|
roles: ["member"],
|
||||||
invitedBy: "admin-user",
|
invitedBy: "admin-user",
|
||||||
|
metadata: { organizationName: "Acme", workspaceName: "Product" },
|
||||||
baseUrl: "https://example.test",
|
baseUrl: "https://example.test",
|
||||||
});
|
});
|
||||||
expect(messages.at(-1)?.template).toBe("invitation");
|
expect(messages.at(-1)?.template).toBe("invitation");
|
||||||
|
expect(await engine.inspectInvitation(invitation.token)).toMatchObject({
|
||||||
|
target: "invited@example.com",
|
||||||
|
metadata: { organizationName: "Acme", workspaceName: "Product" },
|
||||||
|
});
|
||||||
const accepted = await engine.acceptInvitation(invitation.token, {
|
const accepted = await engine.acceptInvitation(invitation.token, {
|
||||||
password: "StrongPassword123",
|
password: "StrongPassword123",
|
||||||
});
|
});
|
||||||
@@ -399,6 +404,7 @@ describe("authentication engine", () => {
|
|||||||
expect(
|
expect(
|
||||||
(await engine.acceptInvitation(invitation.token, { password: "StrongPassword123" })).ok,
|
(await engine.acceptInvitation(invitation.token, { password: "StrongPassword123" })).ok,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
expect(await engine.inspectInvitation(invitation.token)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("customizes delivered token links without application-owned auth APIs", async () => {
|
test("customizes delivered token links without application-owned auth APIs", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global Audio, CSS, CustomEvent, Element, HTMLElement, MutationObserver, URL, clearInterval, clearTimeout, document, fetch, window */
|
/* global Audio, CSS, CustomEvent, Element, Event, HTMLElement, MutationObserver, URL, clearInterval, clearTimeout, document, fetch, window */
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
@@ -149,6 +149,11 @@
|
|||||||
if (input) {
|
if (input) {
|
||||||
input.name = state.config.responseField;
|
input.name = state.config.responseField;
|
||||||
input.value = state.responseToken;
|
input.value = state.responseToken;
|
||||||
|
// Keep the serialized attribute in sync for enhanced forms and DOM
|
||||||
|
// serializers that read attributes rather than the live value property.
|
||||||
|
input.setAttribute("value", state.responseToken);
|
||||||
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.8.14",
|
"version": "0.8.16",
|
||||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|||||||
import { parse } from "@wrnexus/syntax";
|
import { parse } from "@wrnexus/syntax";
|
||||||
|
|
||||||
const componentPath = join(import.meta.dir, "../components/Captcha.wrn");
|
const componentPath = join(import.meta.dir, "../components/Captcha.wrn");
|
||||||
|
const clientPath = join(import.meta.dir, "../assets/client/captcha.js");
|
||||||
|
|
||||||
test("Captcha.wrn parses and exposes the full public contract", async () => {
|
test("Captcha.wrn parses and exposes the full public contract", async () => {
|
||||||
const source = await readFile(componentPath, "utf8");
|
const source = await readFile(componentPath, "utf8");
|
||||||
@@ -70,3 +71,10 @@ test("Captcha.wrn parses and exposes the full public contract", async () => {
|
|||||||
expect(source).not.toContain("captcha.js");
|
expect(source).not.toContain("captcha.js");
|
||||||
expect(source).not.toContain("lifecycle {");
|
expect(source).not.toContain("lifecycle {");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("CAPTCHA response tokens remain visible to enhanced form serializers", async () => {
|
||||||
|
const source = await readFile(clientPath, "utf8");
|
||||||
|
expect(source).toContain('input.setAttribute("value", state.responseToken)');
|
||||||
|
expect(source).toContain('new Event("input", { bubbles: true })');
|
||||||
|
expect(source).toContain('new Event("change", { bubbles: true })');
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.64",
|
"version": "0.8.65",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.35",
|
"version": "0.8.38",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*"
|
"@wrnexus/core": "^0.8.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
var wrn = (window.wrn = window.wrn || {});
|
var wrn = (window.wrn = window.wrn || {});
|
||||||
if (wrn.room) return; // already installed
|
if (wrn.room) return; // already installed
|
||||||
var open = {}; // normalized room+query key -> room connection
|
var open = {}; // normalized room+query key -> room connection
|
||||||
|
var refreshTimer = null;
|
||||||
|
|
||||||
function normalizedQuery(query) {
|
function normalizedQuery(query) {
|
||||||
var params = new URLSearchParams(String(query || "").replace(/^\?/, ""));
|
var params = new URLSearchParams(String(query || "").replace(/^\?/, ""));
|
||||||
@@ -165,6 +166,16 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
if (msg.type === "__open") return setStatus("connected", "is-connected");
|
if (msg.type === "__open") return setStatus("connected", "is-connected");
|
||||||
if (msg.type === "__close") return setStatus("disconnected", "is-disconnected");
|
if (msg.type === "__close") return setStatus("disconnected", "is-disconnected");
|
||||||
if (msg.type === "__error") return setStatus("error", "is-error");
|
if (msg.type === "__error") return setStatus("error", "is-error");
|
||||||
|
window.dispatchEvent(new window.CustomEvent("wrnexus:realtime", {
|
||||||
|
detail: { room: name, message: msg }
|
||||||
|
}));
|
||||||
|
if (el.hasAttribute("data-room-refresh")) {
|
||||||
|
clearTimeout(refreshTimer);
|
||||||
|
refreshTimer = setTimeout(function () {
|
||||||
|
if (typeof window.__wrnexusRefresh === "function") window.__wrnexusRefresh();
|
||||||
|
else location.reload();
|
||||||
|
}, Math.max(50, Number(el.getAttribute("data-room-refresh-delay") || 250)));
|
||||||
|
}
|
||||||
if (!log) return;
|
if (!log) return;
|
||||||
var tpl = templates[msg.type];
|
var tpl = templates[msg.type];
|
||||||
if (tpl == null) tpl = templates[""];
|
if (tpl == null) tpl = templates[""];
|
||||||
|
|||||||
@@ -108,6 +108,20 @@ test("submitting [data-room-send] sends JSON and clears reset fields", () => {
|
|||||||
expect((inputs[1] as unknown as HTMLInputElement).value).toBe("");
|
expect((inputs[1] as unknown as HTMLInputElement).value).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("data-room-refresh softly refreshes and emits a browser event", async () => {
|
||||||
|
const win = boot(`<div data-room="updates" data-room-refresh></div>`) as unknown as Window & {
|
||||||
|
__wrnexusRefresh: () => void;
|
||||||
|
};
|
||||||
|
let events = 0;
|
||||||
|
let refreshes = 0;
|
||||||
|
win.addEventListener("wrnexus:realtime", () => events++);
|
||||||
|
win.__wrnexusRefresh = () => refreshes++;
|
||||||
|
sockets[0]!.fireMessage({ type: "invalidate", resource: "messages" });
|
||||||
|
await Bun.sleep(300);
|
||||||
|
expect(events).toBe(1);
|
||||||
|
expect(refreshes).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("programmatic wrn.room() sends and receives", () => {
|
test("programmatic wrn.room() sends and receives", () => {
|
||||||
const win = boot(`<div></div>`) as unknown as Window & {
|
const win = boot(`<div></div>`) as unknown as Window & {
|
||||||
wrn: {
|
wrn: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.20",
|
"version": "0.8.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.59",
|
"version": "0.8.61",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.8.16",
|
"version": "0.8.18",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => {
|
|||||||
addEventListener("wrnexus:diagnostic",event=>{const detail=event.detail||{};const code=detail.code||"WRN-RUNTIME";const category=String(code).includes("HYDRATE")?"runtime":String(code).includes("ROUTE")?"routing":"compiler";const title=String(code).includes("HYDRATE")?"Hydration diagnostic":"WRNexus diagnostic";const x=issue(String(code),category,"error",title,detail.message||"Framework diagnostic",null,"Open the source location and resolve the reported framework contract.","high",detail);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();});
|
addEventListener("wrnexus:diagnostic",event=>{const detail=event.detail||{};const code=detail.code||"WRN-RUNTIME";const category=String(code).includes("HYDRATE")?"runtime":String(code).includes("ROUTE")?"routing":"compiler";const title=String(code).includes("HYDRATE")?"Hydration diagnostic":"WRNexus diagnostic";const x=issue(String(code),category,"error",title,detail.message||"Framework diagnostic",null,"Open the source location and resolve the reported framework contract.","high",detail);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();});
|
||||||
addEventListener("wrnexus:store-mutation",event=>{const mutation=event.detail||{};const x=issue("stores/mutation","stores","info","Store action: "+(mutation.store||"unknown")+"."+(mutation.action||"direct"),"Changed fields: "+((mutation.changed||[]).join(", ")||"none"),null,"Use the Stores panel to inspect safe client state.","high",mutation);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();});
|
addEventListener("wrnexus:store-mutation",event=>{const mutation=event.detail||{};const x=issue("stores/mutation","stores","info","Store action: "+(mutation.store||"unknown")+"."+(mutation.action||"direct"),"Changed fields: "+((mutation.changed||[]).join(", ")||"none"),null,"Use the Stores panel to inspect safe client state.","high",mutation);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();});
|
||||||
addEventListener("wrnexus:hydrated",event=>{state.runtimeMetrics.hydrationCount+=1;state.runtimeMetrics.hydrationMs+=Number(event.detail?.durationMs||0);});
|
addEventListener("wrnexus:hydrated",event=>{state.runtimeMetrics.hydrationCount+=1;state.runtimeMetrics.hydrationMs+=Number(event.detail?.durationMs||0);});
|
||||||
try{new PerformanceObserver(list=>{for(const entry of list.getEntries()){if(entry.duration>50)state.runtimeMetrics.longTasks.push(entry.duration);}state.runtimeMetrics.longTasks=state.runtimeMetrics.longTasks.slice(-100);}).observe({type:"longtask",buffered:true});}catch{}
|
try{if(PerformanceObserver.supportedEntryTypes?.includes("longtask")){new PerformanceObserver(list=>{for(const entry of list.getEntries()){if(entry.duration>50)state.runtimeMetrics.longTasks.push(entry.duration);}state.runtimeMetrics.longTasks=state.runtimeMetrics.longTasks.slice(-100);}).observe({type:"longtask",buffered:true});}}catch{}
|
||||||
fetch("/__wrnexus/dev-toolbar/platform").then(r=>r.ok?r.json():null).then(data=>{if(!data)return;state.platform=data.platform;state.panels=data.panels||[];const apps=root.querySelector(".wrn-apps");const existing=new Set([...apps.querySelectorAll("[data-category]")].map(button=>button.dataset.category));for(const app of state.panels){if(!existing.has(app.id)){const button=document.createElement("button");button.className="wrn-small";button.dataset.category=app.id;button.textContent=app.title+(app.badge!==undefined?" ("+app.badge+")":"");button.title=app.description||app.title;apps.appendChild(button);}if(app.data!==undefined||app.issues?.length){const payload=app.data!==undefined?app.data:app.issues;state.issues.push(issue("plugin/"+app.id,app.id,"info",app.title,JSON.stringify(payload,null,2),null,app.description||"Plugin-provided development information.","high",{panel:app.id,data:payload}));}}render();}).catch(()=>{});
|
fetch("/__wrnexus/dev-toolbar/platform").then(r=>r.ok?r.json():null).then(data=>{if(!data)return;state.platform=data.platform;state.panels=data.panels||[];const apps=root.querySelector(".wrn-apps");const existing=new Set([...apps.querySelectorAll("[data-category]")].map(button=>button.dataset.category));for(const app of state.panels){if(!existing.has(app.id)){const button=document.createElement("button");button.className="wrn-small";button.dataset.category=app.id;button.textContent=app.title+(app.badge!==undefined?" ("+app.badge+")":"");button.title=app.description||app.title;apps.appendChild(button);}if(app.data!==undefined||app.issues?.length){const payload=app.data!==undefined?app.data:app.issues;state.issues.push(issue("plugin/"+app.id,app.id,"info",app.title,JSON.stringify(payload,null,2),null,app.description||"Plugin-provided development information.","high",{panel:app.id,data:payload}));}}render();}).catch(()=>{});
|
||||||
window.__wrnexusDevToolbar={open(){state.open=true;panel.classList.add("open")},close(){state.open=false;panel.classList.remove("open")},toggle(){state.open=!state.open;panel.classList.toggle("open",state.open)},scan,clear(){state.issues=[];render()},report(){return state.report},highlight,configure(config){state.config=Object.assign(state.config,config||{});shell.dataset.position=state.config.position;try{localStorage.setItem(KEY,JSON.stringify(state.config))}catch{}}};
|
window.__wrnexusDevToolbar={open(){state.open=true;panel.classList.add("open")},close(){state.open=false;panel.classList.remove("open")},toggle(){state.open=!state.open;panel.classList.toggle("open",state.open)},scan,clear(){state.issues=[];render()},report(){return state.report},highlight,configure(config){state.config=Object.assign(state.config,config||{});shell.dataset.position=state.config.position;try{localStorage.setItem(KEY,JSON.stringify(state.config))}catch{}}};
|
||||||
const observer=new MutationObserver(()=>{clearTimeout(observer.timer);observer.timer=setTimeout(()=>{if(!state.open)return;scan()},400)});observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style","src","href","alt","aria-label"]});
|
const observer=new MutationObserver(()=>{clearTimeout(observer.timer);observer.timer=setTimeout(()=>{if(!state.open)return;scan()},400)});observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style","src","href","alt","aria-label"]});
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { DEV_TOOLBAR_RUNTIME } from "../src/client/runtime.ts";
|
||||||
|
|
||||||
|
test("long-task observation is enabled only in browsers that support it", () => {
|
||||||
|
expect(DEV_TOOLBAR_RUNTIME).toContain(
|
||||||
|
'PerformanceObserver.supportedEntryTypes?.includes("longtask")',
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -93,7 +93,9 @@ test("formats large typed JSON prop defaults and remains idempotent", () => {
|
|||||||
}`;
|
}`;
|
||||||
const formatted = formatWrn(source, options);
|
const formatted = formatWrn(source, options);
|
||||||
|
|
||||||
expect(formatted).toContain(`categories: unknown[] = [\n {\n "label": "Platform & Core"`);
|
expect(formatted).toContain(
|
||||||
|
`categories: unknown[] = [\n {\n "label": "Platform & Core"`,
|
||||||
|
);
|
||||||
expect(formatted).toContain(`"apps": [\n {`);
|
expect(formatted).toContain(`"apps": [\n {`);
|
||||||
expect(formatWrn(formatted, options)).toBe(formatted);
|
expect(formatWrn(formatted, options)).toBe(formatted);
|
||||||
expect(() => parse(formatted)).not.toThrow();
|
expect(() => parse(formatted)).not.toThrow();
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ component AdvancedSelect {
|
|||||||
{#if searchable}
|
{#if searchable}
|
||||||
<label class="wrn-next__select-search">
|
<label class="wrn-next__select-search">
|
||||||
<span class="wrn-visually-hidden">{searchPlaceholder}</span>
|
<span class="wrn-visually-hidden">{searchPlaceholder}</span>
|
||||||
<span class="wrn-next__search-icon" aria-hidden="true">⌕</span>
|
<span class="wrn-next__search-icon icon-[lucide--search]" aria-hidden="true"></span>
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
value="{query}"
|
value="{query}"
|
||||||
@@ -473,8 +473,17 @@ component AdvancedSelect {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-next__select-search .wrn-next__search-icon {
|
||||||
|
inset-inline-start: 0.8em;
|
||||||
|
left: auto;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.wrn-next__select-search input {
|
.wrn-next__select-search input {
|
||||||
padding-left: 2.25em;
|
padding-inline-start: 2.4em;
|
||||||
|
padding-inline-end: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrn-next__select-tags {
|
.wrn-next__select-tags {
|
||||||
|
|||||||
@@ -86,6 +86,16 @@ component Sidebar {
|
|||||||
output.close({ source: "drawer" })
|
output.close({ source: "drawer" })
|
||||||
output.toggle({ open: false })
|
output.toggle({ open: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client function revealActiveItem() {
|
||||||
|
document.querySelector(".wrn-sidebar__rail .wrn-sidebar__link--active") && document.querySelector(".wrn-sidebar__rail .wrn-sidebar__link--active").scrollIntoView({ block: "nearest", inline: "nearest", behavior: "auto" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
mount {
|
||||||
|
revealActiveItem()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view {
|
view {
|
||||||
@@ -115,7 +125,7 @@ component Sidebar {
|
|||||||
<p class="wrn-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
|
<p class="wrn-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="wrn-sidebar__link"
|
class='wrn-sidebar__link {isActive(entry) ? "wrn-sidebar__link--active" : ""}'
|
||||||
data-show="!entry.heading"
|
data-show="!entry.heading"
|
||||||
href='{entry.href || "#"}'
|
href='{entry.href || "#"}'
|
||||||
data-wrn-roving-item="true"
|
data-wrn-roving-item="true"
|
||||||
@@ -138,7 +148,7 @@ component Sidebar {
|
|||||||
{#each childrenOf(entry) as child}
|
{#each childrenOf(entry) as child}
|
||||||
<li class="wrn-sidebar__entry">
|
<li class="wrn-sidebar__entry">
|
||||||
<a
|
<a
|
||||||
class="wrn-sidebar__link"
|
class='wrn-sidebar__link {isActive(child) ? "wrn-sidebar__link--active" : ""}'
|
||||||
href='{child.href || "#"}'
|
href='{child.href || "#"}'
|
||||||
data-active='{isActive(child)}'
|
data-active='{isActive(child)}'
|
||||||
aria-current='{isActive(child) ? "page" : "false"}'
|
aria-current='{isActive(child) ? "page" : "false"}'
|
||||||
@@ -163,7 +173,7 @@ component Sidebar {
|
|||||||
{#each childrenOf(child) as leaf}
|
{#each childrenOf(child) as leaf}
|
||||||
<li class="wrn-sidebar__entry">
|
<li class="wrn-sidebar__entry">
|
||||||
<a
|
<a
|
||||||
class="wrn-sidebar__link"
|
class='wrn-sidebar__link {isActive(leaf) ? "wrn-sidebar__link--active" : ""}'
|
||||||
href='{leaf.href || "#"}'
|
href='{leaf.href || "#"}'
|
||||||
data-active='{isActive(leaf)}'
|
data-active='{isActive(leaf)}'
|
||||||
aria-current='{isActive(leaf) ? "page" : "false"}'
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
||||||
@@ -201,7 +211,7 @@ component Sidebar {
|
|||||||
<p class="wrn-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
|
<p class="wrn-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="wrn-sidebar__link"
|
class='wrn-sidebar__link {isActive(entry) ? "wrn-sidebar__link--active" : ""}'
|
||||||
data-show="!entry.heading"
|
data-show="!entry.heading"
|
||||||
href='{entry.href || "#"}'
|
href='{entry.href || "#"}'
|
||||||
data-wrn-roving-item="true"
|
data-wrn-roving-item="true"
|
||||||
@@ -224,7 +234,7 @@ component Sidebar {
|
|||||||
{#each childrenOf(entry) as child}
|
{#each childrenOf(entry) as child}
|
||||||
<li class="wrn-sidebar__entry">
|
<li class="wrn-sidebar__entry">
|
||||||
<a
|
<a
|
||||||
class="wrn-sidebar__link"
|
class='wrn-sidebar__link {isActive(child) ? "wrn-sidebar__link--active" : ""}'
|
||||||
href='{child.href || "#"}'
|
href='{child.href || "#"}'
|
||||||
data-active='{isActive(child)}'
|
data-active='{isActive(child)}'
|
||||||
aria-current='{isActive(child) ? "page" : "false"}'
|
aria-current='{isActive(child) ? "page" : "false"}'
|
||||||
@@ -249,7 +259,7 @@ component Sidebar {
|
|||||||
{#each childrenOf(child) as leaf}
|
{#each childrenOf(child) as leaf}
|
||||||
<li class="wrn-sidebar__entry">
|
<li class="wrn-sidebar__entry">
|
||||||
<a
|
<a
|
||||||
class="wrn-sidebar__link"
|
class='wrn-sidebar__link {isActive(leaf) ? "wrn-sidebar__link--active" : ""}'
|
||||||
href='{leaf.href || "#"}'
|
href='{leaf.href || "#"}'
|
||||||
data-active='{isActive(leaf)}'
|
data-active='{isActive(leaf)}'
|
||||||
aria-current='{isActive(leaf) ? "page" : "false"}'
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ component Badge {
|
|||||||
|
|
||||||
props {
|
props {
|
||||||
label: string = "Badge"
|
label: string = "Badge"
|
||||||
|
humanize: boolean = true
|
||||||
size: string = "md"
|
size: string = "md"
|
||||||
color: string = "primary"
|
color: string = "primary"
|
||||||
variant: string = "solid"
|
variant: string = "solid"
|
||||||
@@ -86,7 +87,7 @@ component Badge {
|
|||||||
{/if}
|
{/if}
|
||||||
{#if !dotOnly}
|
{#if !dotOnly}
|
||||||
<span class="wrn-next__badge-label {truncate ? 'wrn-next__badge-label--truncate' : ''}">
|
<span class="wrn-next__badge-label {truncate ? 'wrn-next__badge-label--truncate' : ''}">
|
||||||
{label}
|
{humanize ? label.replaceAll('_', ' ').replaceAll('-', ' ') : label}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if icon && iconPosition === "end"}
|
{#if icon && iconPosition === "end"}
|
||||||
@@ -142,6 +143,10 @@ component Badge {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-next__badge-label {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
.wrn-next__badge[data-size="xs"] {
|
.wrn-next__badge[data-size="xs"] {
|
||||||
min-height: 1.25rem;
|
min-height: 1.25rem;
|
||||||
padding: 0.15rem 0.4rem;
|
padding: 0.15rem 0.4rem;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ label: string = "Button"
|
|||||||
ariaExpanded: string = ""
|
ariaExpanded: string = ""
|
||||||
ariaControls: string = ""
|
ariaControls: string = ""
|
||||||
title: string = ""
|
title: string = ""
|
||||||
|
tooltipPlacement: string = "top"
|
||||||
autofocus: boolean = false
|
autofocus: boolean = false
|
||||||
controlClass: string = ""
|
controlClass: string = ""
|
||||||
class: string = ""
|
class: string = ""
|
||||||
@@ -93,7 +94,8 @@ label: string = "Button"
|
|||||||
{/if}
|
{/if}
|
||||||
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
|
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
|
||||||
<span
|
<span
|
||||||
class="wrn-btn__tooltip"
|
class="wrn-btn__tooltip wrn-btn__tooltip--{tooltipPlacement}"
|
||||||
|
data-placement="{tooltipPlacement}"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
@@ -154,7 +156,8 @@ label: string = "Button"
|
|||||||
{/if}
|
{/if}
|
||||||
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
|
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
|
||||||
<span
|
<span
|
||||||
class="wrn-btn__tooltip"
|
class="wrn-btn__tooltip wrn-btn__tooltip--{tooltipPlacement}"
|
||||||
|
data-placement="{tooltipPlacement}"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
@@ -469,5 +472,11 @@ label: string = "Button"
|
|||||||
opacity var(--wrn-motion-fast) var(--wrn-ease-standard),
|
opacity var(--wrn-motion-fast) var(--wrn-ease-standard),
|
||||||
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
|
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-btn__tooltip--bottom {
|
||||||
|
inset-block-start: calc(100% + 0.5rem);
|
||||||
|
inset-block-end: auto;
|
||||||
|
transform: translate(-50%, -0.25rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,7 +330,7 @@ size: string = "default"
|
|||||||
font-size: 0.82em;
|
font-size: 0.82em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.wrn-next__field-error:empty {
|
.wrn-next--field[data-invalid="false"] > .wrn-next__field-error {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
|
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.70",
|
"version": "0.8.76",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -153,9 +153,9 @@ describe("@wrnexus/ui redesign component pack", () => {
|
|||||||
|
|
||||||
expect(source).toContain('<slot name="brand" />');
|
expect(source).toContain('<slot name="brand" />');
|
||||||
expect(source).toContain("brand.logo || brand.icon || brand.label || brand.description");
|
expect(source).toContain("brand.logo || brand.icon || brand.label || brand.description");
|
||||||
expect(source.indexOf("brand.logo || brand.icon || brand.label || brand.description")).toBeLessThan(
|
expect(
|
||||||
source.indexOf('<slot name="brand" />'),
|
source.indexOf("brand.logo || brand.icon || brand.label || brand.description"),
|
||||||
);
|
).toBeLessThan(source.indexOf('<slot name="brand" />'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Navbar renders slotted utilities before its built-in actions", async () => {
|
test("Navbar renders slotted utilities before its built-in actions", async () => {
|
||||||
@@ -183,8 +183,8 @@ describe("@wrnexus/ui redesign component pack", () => {
|
|||||||
|
|
||||||
expect(source).toContain("wrn-navbar__mobile-mega-link");
|
expect(source).toContain("wrn-navbar__mobile-mega-link");
|
||||||
expect(source).toContain("item.href || megaData(item).actionHref");
|
expect(source).toContain("item.href || megaData(item).actionHref");
|
||||||
expect(source).toContain('.wrn-navbar__mega { display: none; }');
|
expect(source).toContain(".wrn-navbar__mega { display: none; }");
|
||||||
expect(source).toContain('.wrn-navbar__mobile-mega-link { display: flex; }');
|
expect(source).toContain(".wrn-navbar__mobile-mega-link { display: flex; }");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Navbar supports in-flow floating and elevated menu panels", async () => {
|
test("Navbar supports in-flow floating and elevated menu panels", async () => {
|
||||||
|
|||||||
@@ -241,6 +241,15 @@ test("screenshot-directed canonical components are bundled", () => {
|
|||||||
expect(uiComponentNames()).toEqual(expect.arrayContaining(required));
|
expect(uiComponentNames()).toEqual(expect.arrayContaining(required));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("badge labels are humanized and capitalized by default", () => {
|
||||||
|
const source = readFileSync(uiComponentPath("Badge"), "utf8");
|
||||||
|
const contract = parse(source);
|
||||||
|
const defaults = new Map(contract.props.map((prop) => [prop.name, prop.default]));
|
||||||
|
expect(defaults.get("humanize")).toBe("true");
|
||||||
|
expect(source).toContain("label.replaceAll('_', ' ').replaceAll('-', ' ')");
|
||||||
|
expect(source).toContain("text-transform: capitalize");
|
||||||
|
});
|
||||||
|
|
||||||
test("navbar supports brands, nested menus, actions, utility slots, and public events", () => {
|
test("navbar supports brands, nested menus, actions, utility slots, and public events", () => {
|
||||||
const source = readFileSync(uiComponentPath("Navbar"), "utf8");
|
const source = readFileSync(uiComponentPath("Navbar"), "utf8");
|
||||||
expect(source).toContain('slot name="topbar"');
|
expect(source).toContain('slot name="topbar"');
|
||||||
@@ -638,12 +647,28 @@ test("every bundled UI component compiles", () => {
|
|||||||
|
|
||||||
test("icon buttons expose their accessible label as a hover and focus tooltip", () => {
|
test("icon buttons expose their accessible label as a hover and focus tooltip", () => {
|
||||||
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
||||||
expect(button).toContain('class="wrn-btn__tooltip"');
|
expect(button).toContain('class="wrn-btn__tooltip wrn-btn__tooltip--{tooltipPlacement}"');
|
||||||
expect(button).toContain("{ariaLabel || label}");
|
expect(button).toContain("{ariaLabel || label}");
|
||||||
expect(uiStyles()).toContain(".wrn-btn:hover > .wrn-btn__tooltip");
|
expect(uiStyles()).toContain(".wrn-btn:hover > .wrn-btn__tooltip");
|
||||||
expect(uiStyles()).toContain(".wrn-btn:focus-visible > .wrn-btn__tooltip");
|
expect(uiStyles()).toContain(".wrn-btn:focus-visible > .wrn-btn__tooltip");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("icon button tooltips support downward placement below sticky headers", () => {
|
||||||
|
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
||||||
|
const contract = parse(button);
|
||||||
|
const defaults = new Map(contract.props.map((prop) => [prop.name, prop.default]));
|
||||||
|
expect(defaults.get("tooltipPlacement")).toBe('"top"');
|
||||||
|
expect(button).toContain('data-placement="{tooltipPlacement}"');
|
||||||
|
expect(uiStyles()).toContain(".wrn-btn__tooltip--bottom");
|
||||||
|
expect(uiStyles()).toContain("inset-block-start: calc(100% + 0.5rem)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("textarea does not reserve validation space while its error is empty", () => {
|
||||||
|
const textarea = readFileSync(uiComponentPath("Textarea"), "utf8");
|
||||||
|
expect(textarea).toContain('.wrn-next--field[data-invalid="false"] > .wrn-next__field-error');
|
||||||
|
expect(textarea).not.toContain(".wrn-next__field-error:empty");
|
||||||
|
});
|
||||||
|
|
||||||
test("button links navigate by default and only download when the native attribute is passed", () => {
|
test("button links navigate by default and only download when the native attribute is passed", () => {
|
||||||
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
||||||
expect(button).not.toMatch(/^\s+download\s*=/m);
|
expect(button).not.toMatch(/^\s+download\s*=/m);
|
||||||
@@ -2043,6 +2068,11 @@ test("advanced select opens, filters, and selects without reactive handler error
|
|||||||
expect(dom.querySelector(".wrn-next__select-dropdown")?.getAttribute("data-show")).toBe("true");
|
expect(dom.querySelector(".wrn-next__select-dropdown")?.getAttribute("data-show")).toBe("true");
|
||||||
expect(dom.querySelector('[role="listbox"]')).not.toBeNull();
|
expect(dom.querySelector('[role="listbox"]')).not.toBeNull();
|
||||||
const search = dom.querySelector(".wrn-next__select-search input") as HTMLInputElement;
|
const search = dom.querySelector(".wrn-next__select-search input") as HTMLInputElement;
|
||||||
|
expect(
|
||||||
|
dom
|
||||||
|
.querySelector(".wrn-next__select-search .wrn-next__search-icon")
|
||||||
|
?.classList.contains("icon-[lucide--search]"),
|
||||||
|
).toBe(true);
|
||||||
search.value = "engine";
|
search.value = "engine";
|
||||||
search.dispatchEvent(
|
search.dispatchEvent(
|
||||||
new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
|
new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
|
||||||
@@ -3281,6 +3311,16 @@ test("sidebar composes Drawer for its off-canvas presentation", async () => {
|
|||||||
expect(source).toContain("<Drawer");
|
expect(source).toContain("<Drawer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("sidebar reveals the active item when a long navigation rail mounts", () => {
|
||||||
|
const source = readFileSync(uiComponentPath("Sidebar"), "utf8");
|
||||||
|
expect(source).toContain("lifecycle {");
|
||||||
|
expect(source).toContain("revealActiveItem()");
|
||||||
|
expect(source).toContain(".wrn-sidebar__link--active");
|
||||||
|
expect(source).toContain(
|
||||||
|
'scrollIntoView({ block: "nearest", inline: "nearest", behavior: "auto" })',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("scrollspy renders section links and marks the runtime observation target", async () => {
|
test("scrollspy renders section links and marks the runtime observation target", async () => {
|
||||||
const source = readFileSync(uiComponentPath("Scrollspy"), "utf8");
|
const source = readFileSync(uiComponentPath("Scrollspy"), "utf8");
|
||||||
const html = await renderComponent(source, {
|
const html = await renderComponent(source, {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.8.11",
|
"version": "0.8.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const VALIDATE_RUNTIME = String.raw`
|
|||||||
if (el && el.setAttribute) {
|
if (el && el.setAttribute) {
|
||||||
if (err) { el.setAttribute("aria-invalid", "true"); if (el.classList) el.classList.add("wrn-invalid"); }
|
if (err) { el.setAttribute("aria-invalid", "true"); if (el.classList) el.classList.add("wrn-invalid"); }
|
||||||
else { el.removeAttribute("aria-invalid"); if (el.classList) el.classList.remove("wrn-invalid"); }
|
else { el.removeAttribute("aria-invalid"); if (el.classList) el.classList.remove("wrn-invalid"); }
|
||||||
var field = el.closest && el.closest(".wrn-next--field, .wrn-next--choice-field");
|
var field = el.closest && el.closest(".wrn-next--field, .wrn-next--choice-field, [data-component]");
|
||||||
if (field) field.setAttribute("data-invalid", err ? "true" : "false");
|
if (field) field.setAttribute("data-invalid", err ? "true" : "false");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user