release: WRNexusJS 0.3.5
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("/");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user