release: WRNexusJS 0.3.5

This commit is contained in:
2026-07-24 12:46:44 +05:30
parent 44ba847210
commit c81dedff17
2114 changed files with 65790 additions and 150559 deletions
+4 -4
View File
@@ -3,7 +3,7 @@ import type { Db, ExecResult } from "@wrnexus/db";
import { users } from "./schema.ts";
export async function GetUserByEmail(db: Db, args: { email: string }): Promise<{ id: number; email: string; name: string; active: boolean; passwordHash: string; createdAt: Date } | null> {
return (await db.one("SELECT * FROM users WHERE email = $1", [args.email], users)) as { id: number; email: string; name: string; active: boolean; passwordHash: string; createdAt: Date } | null;
return (await db.one("SELECT * FROM users WHERE email = ?", [args.email], users)) as { id: number; email: string; name: string; active: boolean; passwordHash: string; createdAt: Date } | null;
}
export async function ListUsers(db: Db): Promise<{ id: number; name: string; active: boolean }[]> {
@@ -11,13 +11,13 @@ export async function ListUsers(db: Db): Promise<{ id: number; name: string; act
}
export async function CountActive(db: Db, args: { active: boolean }): Promise<{ n: number } | null> {
return (await db.one("SELECT COUNT(*) AS n FROM users WHERE active = $1", [args.active])) as { n: number } | null;
return (await db.one("SELECT COUNT(*) AS n FROM users WHERE active = ?", [args.active])) as { n: number } | null;
}
export async function CreateUser(db: Db, args: { email: string; name: string; active: boolean }): Promise<ExecResult> {
return db.exec("INSERT INTO users (email, name, active) VALUES ($1, $2, $3)", [args.email, args.name, args.active]);
return db.exec("INSERT INTO users (email, name, active) VALUES (?, ?, ?)", [args.email, args.name, args.active]);
}
export async function DeactivateUser(db: Db, args: { id: number }): Promise<ExecResult> {
return db.exec("UPDATE users SET active = 0 WHERE id = $1", [args.id]);
return db.exec("UPDATE users SET active = 0 WHERE id = ?", [args.id]);
}
+233
View File
@@ -0,0 +1,233 @@
// Component event test page. Route: /test
//
// Events bubble from the exact ComboBox or AdvancedSelect that emitted them.
// The <section> below listens once for every child component, while the first
// ComboBox also demonstrates a listener attached directly to one component.
page Test {
layout = "public"
state teams = [{ value: "design", label: "Design", description: "Product and visual design", icon: "icon-[lucide--palette]" }, { value: "engineering", label: "Engineering", description: "Platform engineering", icon: "icon-[lucide--code-2]" }, { value: "growth", label: "Growth", description: "Marketing and customer growth", icon: "icon-[lucide--trending-up]" }, { value: "support", label: "Support", description: "Customer operations", icon: "icon-[lucide--life-buoy]" }]
state eventName = "No event yet"
state eventSource = "Interact with a component"
state eventPayload = "{}"
state selectedTeam = ""
seo {
title = "Component event test"
description = "Test ComboBox and AdvancedSelect events on one WRN page."
canonical = "/test"
}
functions {
function handleComponentEvent(event) {
eventName = event.type
eventSource = event.target.getAttribute("data-field") || event.detail.component
eventPayload = JSON.stringify(event.detail, null, 2)
}
function handleTeamSelect(event) {
selectedTeam = event.detail.value
}
}
view {
<header class="test-hero">
<span>WRN component events</span>
<h1>ComboBox and AdvancedSelect event test</h1>
<p>
This page contains multiple component instances. Every component has a
unique <code>id</code> and <code>data-field</code>, so the event source
is always identifiable.
</p>
</header>
<!--
Event delegation: these eight listeners receive events from every
ComboBox and AdvancedSelect inside this section because the custom events
bubble from their component roots.
-->
<section
class="test-grid"
@search="handleComponentEvent(event)"
@select="handleComponentEvent(event)"
@change="handleComponentEvent(event)"
@clear="handleComponentEvent(event)"
@open="handleComponentEvent(event)"
@close="handleComponentEvent(event)"
@load="handleComponentEvent(event)"
@error="handleComponentEvent(event)"
>
<article class="test-card">
<span class="test-card__type">Direct listener</span>
<h2>Team ComboBox</h2>
<p>
Its <code>@select</code> listener updates the selected-team
text below. The delegated section listener also receives the event.
</p>
<ComboBox
id="team-combobox"
data-field="team-combobox"
name="team"
label="Choose a team"
placeholder="Search teams"
options="{teams}"
optionTemplate="icon"
@select="handleTeamSelect(event)"
/>
<output class="test-selection">
Selected team: {selectedTeam || "None"}
</output>
</article>
<article class="test-card">
<span class="test-card__type">Second instance</span>
<h2>Reviewer ComboBox</h2>
<p>
This is another ComboBox on the same page. Its events remain
distinguishable through <code>data-field="reviewer-combobox"</code>.
</p>
<ComboBox
id="reviewer-combobox"
data-field="reviewer-combobox"
name="reviewer"
label="Choose a reviewer team"
placeholder="Search reviewer teams"
options="{teams}"
/>
</article>
<article class="test-card">
<span class="test-card__type">Multiple selection</span>
<h2>Project teams</h2>
<p>
AdvancedSelect emits the same event names. Multiple values are
available through <code>event.detail.values</code>.
</p>
<AdvancedSelect
id="project-teams"
data-field="project-teams"
name="projectTeams"
label="Assign project teams"
placeholder="Choose one or more teams"
options="{teams}"
multiple
showCounter
maxSelections="3"
optionTemplate="icon"
/>
</article>
<aside class="event-monitor" aria-live="polite">
<div>
<span>Latest event</span>
<strong>{eventName}</strong>
</div>
<div>
<span>Source component</span>
<strong>{eventSource}</strong>
</div>
<pre><code>{eventPayload}</code></pre>
</aside>
</section>
}
style {
.test-hero {
display: grid;
max-width: 52rem;
gap: 0.75rem;
margin-block: 3rem 2rem;
}
.test-hero > span,
.test-card__type,
.event-monitor span {
color: var(--wire-color-primary);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.test-hero h1,
.test-card h2,
.test-hero p,
.test-card p {
margin: 0;
}
.test-hero p,
.test-card p {
color: var(--wire-color-muted);
line-height: 1.65;
}
.test-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 4rem;
}
.test-card,
.event-monitor {
display: grid;
align-content: start;
gap: 1rem;
padding: clamp(1rem, 3vw, 1.5rem);
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-lg);
background: var(--wire-color-surface);
box-shadow: var(--wire-shadow-1);
}
.test-selection {
color: var(--wire-color-muted);
font-size: 0.82rem;
}
.event-monitor {
position: sticky;
bottom: 1rem;
grid-column: 1 / -1;
grid-template-columns: repeat(2, minmax(0, 1fr));
background: color-mix(in srgb, var(--wire-color-surface) 94%, var(--wire-color-primary));
}
.event-monitor > div {
display: grid;
gap: 0.35rem;
}
.event-monitor pre {
grid-column: 1 / -1;
max-height: 18rem;
overflow: auto;
margin: 0;
padding: 1rem;
border-radius: var(--wire-radius-md);
background: var(--wire-color-bg);
font-size: 0.75rem;
line-height: 1.65;
white-space: pre-wrap;
}
@media (max-width: 760px) {
.test-grid {
grid-template-columns: 1fr;
}
.event-monitor {
grid-template-columns: 1fr;
}
.event-monitor pre {
grid-column: auto;
}
}
}
}
+45 -13
View File
@@ -1,6 +1,5 @@
// AUTO-GENERATED by `wrnexus dev` do not edit.
// Typed routes: a compile-time map of every page path to its [param] types,
// plus an href() builder that fills params and rejects unknown paths.
// AUTO-GENERATED by `wrnexus dev` - do not edit.
// Typed routes support required, optional, and catch-all parameters.
export interface Routes {
"/": Record<string, never>;
@@ -10,22 +9,55 @@ export interface Routes {
"/hello": Record<string, never>;
"/login": Record<string, never>;
"/reactive": Record<string, never>;
"/test": Record<string, never>;
"/ui": Record<string, never>;
}
export type RoutePath = keyof Routes;
type RouteValue = string | readonly string[] | undefined;
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
if (value === undefined) return "";
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
return values.map((part) => encodeURIComponent(part)).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: Routes[P] extends Record<string, never> ? [] : [params: Routes[P]]
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, string>;
return String(path)
.split("/")
.map((seg) =>
seg.startsWith("[") && seg.endsWith("]")
? encodeURIComponent(params[seg.slice(1, -1)] ?? "")
: seg,
)
.join("/");
const params = (args[0] ?? {}) as Record<string, RouteValue>;
const output: string[] = [];
for (const segment of String(path).split("/").filter(Boolean)) {
let name: string | undefined;
let optional = false;
let catchAll = false;
if (segment.startsWith("[[") && segment.endsWith("]]")) {
optional = true;
name = segment.slice(2, -2);
} else if (segment.startsWith("[") && segment.endsWith("]")) {
name = segment.slice(1, -1);
if (name.endsWith("?")) {
optional = true;
name = name.slice(0, -1);
}
}
if (!name) {
output.push(segment);
continue;
}
if (name.startsWith("...")) {
catchAll = true;
name = name.slice(3);
}
const value = params[name];
if (value === undefined && optional) continue;
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
output.push(encodeRouteValue(value, catchAll));
}
return "/" + output.filter(Boolean).join("/");
}