feat(ui): build remaining component scaffolds

This commit is contained in:
2026-08-09 14:18:43 +05:30
parent 1660044ed2
commit 2e5c0cc953
12 changed files with 574 additions and 305 deletions
+2 -1
View File
@@ -24,7 +24,7 @@ import { pathToFileURL } from "node:url";
import { compileWireFile } from "@wrnexus/compiler";
import { createContext } from "@wrnexus/core";
import type { Context } from "@wrnexus/core";
import { getReactiveRuntime } from "@wrnexus/csr";
import { getComponentControllerRuntime, getReactiveRuntime } from "@wrnexus/csr";
import { startServer } from "@wrnexus/dev-server";
import {
loadAppConfig,
@@ -100,6 +100,7 @@ export function mountHtml(html: string): {
g.HTMLSelectElement = win.HTMLSelectElement;
g.HTMLTextAreaElement = win.HTMLTextAreaElement;
(0, eval)(getReactiveRuntime());
(0, eval)(getComponentControllerRuntime());
(win as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
win.document,
);
+9 -18
View File
@@ -1,6 +1,6 @@
# WRNexus UI component reference
This reference is generated from the 103 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
This reference is generated from the 102 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
## Advanced-forms
@@ -22,15 +22,6 @@ Editable autocomplete combobox with local and remote suggestions.
- Slots: None
- Outputs: `search({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### CopyMarkup
Theme-aware, responsive copy markup component.
- Mount: `data-component="CopyMarkup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Copy Markup"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "text"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `copy({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### InputNumber
Theme-aware, responsive input number component.
@@ -308,9 +299,9 @@ Present responsive chronological activity, milestones, or workflow status with r
Theme-aware, responsive tree view component.
- Mount: `data-component="TreeView"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Tree View"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Props: `title: string = "Tree View"`, `description: string = ""`, `items: unknown[] = []`, `valueKey: string = "value"`, `labelKey: string = "label"`, `defaultExpanded: unknown[] = []`, `selected: string = ""`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `expand({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `collapse({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
- Outputs: `select({ item: object; value: string; sourceEvent?: Event })`, `toggle({ item: object; value: string; expanded: boolean; sourceEvent?: Event })`, `expand({ item: object; value: string; sourceEvent?: Event })`, `collapse({ item: object; value: string; sourceEvent?: Event })`
## Core
@@ -505,27 +496,27 @@ Theme-aware, responsive time picker component.
Theme-aware, responsive chart component.
- Mount: `data-component="Chart"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Chart"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Props: `title: string = "Chart"`, `description: string = ""`, `items: unknown[] = []`, `valueKey: string = "value"`, `labelKey: string = "label"`, `height: number = 240`, `showLegend: boolean = true`, `showValues: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "bar"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `dataPointClick({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `legendToggle({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
- Outputs: `select({ item: object; index: number; sourceEvent?: Event })`, `dataPointClick({ item: object; index: number; sourceEvent?: Event })`, `legendToggle({ item: object; index: number; hidden: boolean; sourceEvent?: Event })`
### Clipboard
Theme-aware, responsive clipboard component.
- Mount: `data-component="Clipboard"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Clipboard"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Props: `value: string = ""`, `label: string = "Copy"`, `copiedLabel: string = "Copied"`, `errorLabel: string = "Copy failed"`, `title: string = "Clipboard"`, `description: string = ""`, `code: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `disabled: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: `copy({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
- Outputs: `copy({ value: string; sourceEvent?: Event })`, `success({ value: string; sourceEvent?: Event })`, `error({ error: Error | string; value: string; sourceEvent?: Event })`
### Confetti
Theme-aware, responsive confetti component.
- Mount: `data-component="Confetti"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Confetti"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Props: `label: string = "Celebrate"`, `duration: number = 1200`, `count: number = 24`, `autoStart: boolean = false`, `disabled: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
- Slots: `default`
- Outputs: `start({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
- Outputs: `start({ sourceEvent?: Event; duration: number })`, `complete({ sourceEvent?: Event; duration: number })`
### DataMap
-5
View File
@@ -143,11 +143,6 @@
"category": "overlays",
"purpose": "Open an accessible keyboard-aware action menu from pointer or keyboard context interactions."
},
{
"name": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component."
},
{
"name": "CustomScrollbar",
"category": "layout",
+3 -2
View File
@@ -1,11 +1,12 @@
{
"generatedFrom": "framework remediation 0.8.7",
"removedCount": 5,
"removedCount": 6,
"replacements": {
"AdvancedDatePicker": "DatePicker",
"AdvancedRangeSlider": "RangeSlider",
"FileUpload": "FileInput",
"Toast": "Toaster",
"ToastNotifications": "Toaster"
"ToastNotifications": "Toaster",
"CopyMarkup": "Clipboard"
}
}
+170 -199
View File
@@ -1,6 +1,6 @@
{
"generatedFrom": "packages/ui/components/*.wrn",
"count": 103,
"count": 102,
"components": [
{
"name": "Accordion",
@@ -2852,20 +2852,6 @@
"category": "integrations",
"purpose": "Theme-aware, responsive chart component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "title",
"type": "string",
@@ -2888,12 +2874,61 @@
"options": []
},
{
"name": "variant",
"name": "valueKey",
"type": "string",
"required": false,
"default": "\"value\"",
"options": []
},
{
"name": "labelKey",
"type": "string",
"required": false,
"default": "\"label\"",
"options": []
},
{
"name": "height",
"type": "number",
"required": false,
"default": "240",
"options": []
},
{
"name": "showLegend",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "showValues",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"bar\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -2908,15 +2943,15 @@
"outputs": [
{
"name": "select",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
"payloadType": "{ item: object; index: number; sourceEvent?: Event }"
},
{
"name": "dataPointClick",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
"payloadType": "{ item: object; index: number; sourceEvent?: Event }"
},
{
"name": "legendToggle",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
"payloadType": "{ item: object; index: number; hidden: boolean; sourceEvent?: Event }"
}
],
"events": [
@@ -3276,17 +3311,31 @@
"purpose": "Theme-aware, responsive clipboard component.",
"props": [
{
"name": "size",
"name": "value",
"type": "string",
"required": false,
"default": "\"default\"",
"default": "\"\"",
"options": []
},
{
"name": "color",
"name": "label",
"type": "string",
"required": false,
"default": "\"primary\"",
"default": "\"Copy\"",
"options": []
},
{
"name": "copiedLabel",
"type": "string",
"required": false,
"default": "\"Copied\"",
"options": []
},
{
"name": "errorLabel",
"type": "string",
"required": false,
"default": "\"Copy failed\"",
"options": []
},
{
@@ -3304,19 +3353,33 @@
"options": []
},
{
"name": "items",
"type": "unknown[]",
"name": "code",
"type": "boolean",
"required": false,
"default": "[]",
"default": "true",
"options": []
},
{
"name": "variant",
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "disabled",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "class",
"type": "string",
@@ -3331,15 +3394,15 @@
"outputs": [
{
"name": "copy",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
"payloadType": "{ value: string; sourceEvent?: Event }"
},
{
"name": "success",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ value: string; sourceEvent?: Event }"
},
{
"name": "error",
"payloadType": "{ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string"
"payloadType": "{ error: Error | string; value: string; sourceEvent?: Event }"
}
],
"events": [
@@ -4006,6 +4069,41 @@
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component.",
"props": [
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Celebrate\"",
"options": []
},
{
"name": "duration",
"type": "number",
"required": false,
"default": "1200",
"options": []
},
{
"name": "count",
"type": "number",
"required": false,
"default": "24",
"options": []
},
{
"name": "autoStart",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "disabled",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "size",
"type": "string",
@@ -4020,34 +4118,6 @@
"default": "\"primary\"",
"options": []
},
{
"name": "title",
"type": "string",
"required": false,
"default": "\"Confetti\"",
"options": []
},
{
"name": "description",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "items",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -4062,11 +4132,11 @@
"outputs": [
{
"name": "start",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ sourceEvent?: Event; duration: number }"
},
{
"name": "complete",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ sourceEvent?: Event; duration: number }"
}
],
"events": [
@@ -4303,126 +4373,6 @@
],
"source": "components/ContextMenu.wrn"
},
{
"name": "CopyMarkup",
"mount": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Copy Markup\"",
"options": []
},
{
"name": "name",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "value",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "placeholder",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "type",
"type": "string",
"required": false,
"default": "\"text\"",
"options": []
},
{
"name": "min",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "max",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "step",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "disabled",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "required",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "class",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
}
],
"slots": [],
"outputs": [
{
"name": "copy",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
},
{
"name": "success",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
},
{
"name": "error",
"payloadType": "{ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string"
}
],
"events": [
"copy",
"success",
"error"
],
"source": "components/CopyMarkup.wrn"
},
{
"name": "CustomScrollbar",
"mount": "CustomScrollbar",
@@ -13973,20 +13923,6 @@
"category": "base",
"purpose": "Theme-aware, responsive tree view component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "title",
"type": "string",
@@ -14009,12 +13945,47 @@
"options": []
},
{
"name": "variant",
"name": "valueKey",
"type": "string",
"required": false,
"default": "\"value\"",
"options": []
},
{
"name": "labelKey",
"type": "string",
"required": false,
"default": "\"label\"",
"options": []
},
{
"name": "defaultExpanded",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "selected",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -14029,19 +14000,19 @@
"outputs": [
{
"name": "select",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
"payloadType": "{ item: object; value: string; sourceEvent?: Event }"
},
{
"name": "toggle",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ item: object; value: string; expanded: boolean; sourceEvent?: Event }"
},
{
"name": "expand",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ item: object; value: string; sourceEvent?: Event }"
},
{
"name": "collapse",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ item: object; value: string; sourceEvent?: Event }"
}
],
"events": [
+93 -11
View File
@@ -1,25 +1,107 @@
component Chart {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
dataPointClick(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
legendToggle(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
select(payload: { item: object; index: number; sourceEvent?: Event })
dataPointClick(payload: { item: object; index: number; sourceEvent?: Event })
legendToggle(payload: { item: object; index: number; hidden: boolean; sourceEvent?: Event })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Chart"
description: string = ""
items: unknown[] = []
variant: string = "default"
valueKey: string = "value"
labelKey: string = "label"
height: number = 240
showLegend: boolean = true
showValues: boolean = true
size: string = "default"
color: string = "primary"
variant: string = "bar"
class: string = ""
}
state hiddenIndexes = []
functions {
shared function numericValue(item) { return Math.max(0, Number(item[valueKey]) || 0) }
shared function maximumValue() {
return Math.max(1, ...items.map(function (item) { return numericValue(item) }))
}
shared function isHidden(index) { return hiddenIndexes.includes(index) }
shared function barWidth() { return items.length ? Math.max(4, 80 / items.length) : 80 }
shared function barX(index) { return 10 + index * (80 / Math.max(1, items.length)) }
shared function barHeight(item) { return numericValue(item) / maximumValue() * 72 }
shared function barY(item) { return 84 - barHeight(item) }
client function selectPoint(item, index, sourceEvent) {
output.dataPointClick({ item: item, index: index, sourceEvent: sourceEvent })
output.select({ item: item, index: index, sourceEvent: sourceEvent })
}
client function toggleLegend(item, index, sourceEvent, next) {
next = hiddenIndexes.slice()
if (next.includes(index)) {
next = next.filter(function (value) { return value !== index })
} else {
next.push(index)
}
hiddenIndexes = next
output.legendToggle({ item: item, index: index, hidden: next.includes(index), sourceEvent: sourceEvent })
}
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chart wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<figure {...attrs} data-ui-component="Chart" data-variant='{variant}' class='wire-chart {class}'>
{#if title || description}
<figcaption class="wire-chart__header">
{#if title}<strong>{title}</strong>{/if}
{#if description}<span>{description}</span>{/if}
</figcaption>
{/if}
<svg class="wire-chart__plot" viewBox="0 0 100 100" role="img" aria-label='{title}' style='height:{height}px'>
<line x1="8" y1="84" x2="96" y2="84" class="wire-chart__axis" />
{#each items as item, index}
<g data-hidden='{isHidden(index) ? "true" : "false"}'>
<rect
x='{barX(index)}' y='{barY(item)}' width='{barWidth()}' height='{barHeight(item)}'
rx="1.5" class="wire-chart__bar" role="button" tabindex="0"
aria-label='{item[labelKey] + ": " + numericValue(item)}'
@click='selectPoint(item, index, event)'
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectPoint(item, index, event) }'
/>
{#if showValues}<text x='{barX(index) + barWidth() / 2}' y='{barY(item) - 3}' class="wire-chart__value">{numericValue(item)}</text>{/if}
<text x='{barX(index) + barWidth() / 2}' y="94" class="wire-chart__label">{item[labelKey]}</text>
</g>
{/each}
</svg>
{#if showLegend}
<div class="wire-chart__legend" aria-label="Chart legend">
{#each items as item, index}
<button type="button" aria-pressed='{isHidden(index) ? "false" : "true"}' @click='toggleLegend(item, index, event)'>
<span aria-hidden="true"></span>{item[labelKey]}
</button>
{/each}
</div>
{/if}
<slot />
</section>
</figure>
}
style {
.wire-chart { display: grid; gap: 1rem; min-width: 0; margin: 0; padding: 1rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); color: var(--wire-color-text); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); }
.wire-chart__header { display: grid; gap: 0.2rem; }
.wire-chart__header > span { color: var(--wire-color-muted); font-size: 0.8rem; }
.wire-chart__plot { display: block; width: 100%; min-height: 12rem; overflow: visible; }
.wire-chart__axis { stroke: var(--wire-color-border); stroke-width: 0.5; }
.wire-chart__bar { fill: var(--wire-color-primary); cursor: pointer; transition: opacity var(--wire-motion-fast), transform var(--wire-motion-fast); transform-box: fill-box; transform-origin: bottom; }
.wire-chart__bar:hover { opacity: 0.82; transform: scaleY(1.02); }
.wire-chart__bar:focus-visible { outline: 1.5px solid var(--wire-color-focus); outline-offset: 1px; }
.wire-chart__plot g[data-hidden="true"] { opacity: 0.18; pointer-events: none; }
.wire-chart__value, .wire-chart__label { fill: var(--wire-color-text); font-size: 3.5px; text-anchor: middle; }
.wire-chart__label { fill: var(--wire-color-muted); }
.wire-chart__legend { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wire-chart__legend button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.5rem; border: 1px solid var(--wire-color-border); border-radius: 999px; color: var(--wire-color-text); background: transparent; font: inherit; font-size: 0.75rem; cursor: pointer; }
.wire-chart__legend button > span { width: 0.65rem; height: 0.65rem; border-radius: 0.2rem; background: var(--wire-color-primary); }
.wire-chart__legend button[aria-pressed="false"] { opacity: 0.5; }
}
}
+83 -11
View File
@@ -1,25 +1,97 @@
component Clipboard {
outputs {
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
copy(payload: { value: string; sourceEvent?: Event })
success(payload: { value: string; sourceEvent?: Event })
error(payload: { error: Error | string; value: string; sourceEvent?: Event })
}
props {
size: string = "default"
color: string = "primary"
value: string = ""
label: string = "Copy"
copiedLabel: string = "Copied"
errorLabel: string = "Copy failed"
title: string = "Clipboard"
description: string = ""
items: unknown[] = []
variant: string = "default"
code: boolean = true
size: string = "default"
color: string = "primary"
disabled: boolean = false
class: string = ""
}
state status = "idle"
functions {
client function resetStatus() { status = "idle" }
client function copySucceeded() {
status = "success"
output.success({ value: value })
setTimeout(resetStatus, 1600)
}
client function copyFailed(error) {
status = "error"
output.error({ error: error, value: value })
setTimeout(resetStatus, 1600)
}
client function copyValue(sourceEvent) {
if (disabled || status === "copying") {
return
}
status = "copying"
output.copy({ value: value, sourceEvent: sourceEvent })
if (!window.navigator.clipboard || !window.navigator.clipboard.writeText) {
copyFailed(new Error("Clipboard API is unavailable"))
return
}
return window.navigator.clipboard.writeText(value).then(copySucceeded).catch(copyFailed)
}
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--clipboard wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<section
{...attrs}
data-ui-component="Clipboard"
data-status='{status}'
data-size='{size}'
data-color='{color}'
class='wire-clipboard {class}'
>
{#if title}<strong class="wire-clipboard__title">{title}</strong>{/if}
{#if description}<p class="wire-clipboard__description">{description}</p>{/if}
<div class="wire-clipboard__control">
{#if code}
<code class="wire-clipboard__value">{value}</code>
{:else}
<span class="wire-clipboard__value">{value}</span>
{/if}
<button
type="button"
class="wire-clipboard__button"
disabled='{disabled}'
aria-label='{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}'
@click='copyValue(event)'
>
<span class="icon-[lucide--copy] wire-clipboard__icon" data-show='status !== "success"' aria-hidden="true"></span>
<span class="icon-[lucide--check] wire-clipboard__icon" data-show='status === "success"' aria-hidden="true"></span>
<span>{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}</span>
</button>
</div>
<slot />
</section>
}
style {
.wire-clipboard { display: grid; gap: 0.5rem; color: var(--wire-color-text); }
.wire-clipboard__title { font-size: 0.875rem; }
.wire-clipboard__description { margin: 0; color: var(--wire-color-muted); font-size: 0.8rem; }
.wire-clipboard__control { display: flex; min-width: 0; align-items: stretch; overflow: hidden; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); }
.wire-clipboard__value { min-width: 0; flex: 1; overflow: auto; padding: 0.7rem 0.85rem; color: var(--wire-color-text); background: var(--wire-color-surface-2); font: inherit; font-family: var(--wire-font-mono, ui-monospace, monospace); white-space: nowrap; }
.wire-clipboard__button { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.6rem 0.85rem; border: 0; border-left: 1px solid var(--wire-color-border); color: var(--wire-color-primary); background: transparent; font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer; }
.wire-clipboard__button:hover { background: var(--wire-color-primary-soft); }
.wire-clipboard__button:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: -3px; }
.wire-clipboard__button:disabled { cursor: not-allowed; opacity: 0.55; }
.wire-clipboard[data-status="success"] .wire-clipboard__button { color: var(--wire-color-success); }
.wire-clipboard[data-status="error"] .wire-clipboard__button { color: var(--wire-color-danger); }
.wire-clipboard__icon { width: 1rem; height: 1rem; flex: none; }
}
}
+59 -11
View File
@@ -1,24 +1,72 @@
component Confetti {
outputs {
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
start(payload: { sourceEvent?: Event; duration: number })
complete(payload: { sourceEvent?: Event; duration: number })
}
props {
label: string = "Celebrate"
duration: number = 1200
count: number = 24
autoStart: boolean = false
disabled: boolean = false
size: string = "default"
color: string = "primary"
title: string = "Confetti"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
state running = false
functions {
shared function pieces() { return Array.from({ length: Math.max(1, Math.min(60, count)) }) }
shared function pieceStyle(index) {
return "--wire-confetti-index:" + index + ";--wire-confetti-x:" + ((index * 37) % 100) + ";--wire-confetti-delay:" + ((index * 29) % 240) + "ms;"
}
client function finish(sourceEvent) {
running = false
output.complete({ sourceEvent: sourceEvent, duration: duration })
}
client function start(sourceEvent) {
if (disabled || running) {
return
}
running = true
output.start({ sourceEvent: sourceEvent, duration: duration })
setTimeout(finish, duration, sourceEvent)
}
}
lifecycle {
mount { if (autoStart) { start(null) } }
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--confetti wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<div {...attrs} data-ui-component="Confetti" data-running='{running ? "true" : "false"}' class='wire-confetti {class}' style='--wire-confetti-duration:{duration}ms'>
<button type="button" class="wire-confetti__trigger" disabled='{disabled}' @click='start(event)'>
<span class="icon-[lucide--party-popper]" aria-hidden="true"></span><span>{label}</span>
</button>
<div class="wire-confetti__burst" aria-hidden="true">
{#each pieces() as piece, index}<i style='{pieceStyle(index)}'></i>{/each}
</div>
<slot />
</section>
</div>
}
style {
.wire-confetti { position: relative; display: inline-flex; }
.wire-confetti__trigger { display: inline-flex; min-height: 2.5rem; align-items: center; gap: 0.5rem; padding: 0.55rem 0.85rem; border: 1px solid var(--wire-color-primary); border-radius: var(--wire-radius); color: var(--wire-color-on-primary); background: var(--wire-color-primary); font: inherit; font-weight: 700; cursor: pointer; }
.wire-confetti__trigger > span:first-child { width: 1rem; height: 1rem; }
.wire-confetti__trigger:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: 2px; }
.wire-confetti__trigger:disabled { cursor: not-allowed; opacity: 0.55; }
.wire-confetti__burst { position: absolute; inset: 50% 0 auto; height: 0; pointer-events: none; }
.wire-confetti__burst > i { position: absolute; left: calc(var(--wire-confetti-x) * 1%); width: 0.45rem; height: 0.7rem; border-radius: 0.1rem; opacity: 0; background: hsl(calc(var(--wire-confetti-index) * 47deg) 80% 55%); transform: translate(-50%, 0) rotate(calc(var(--wire-confetti-index) * 23deg)); }
.wire-confetti[data-running="true"] .wire-confetti__burst > i { animation: wire-confetti-burst var(--wire-confetti-duration) cubic-bezier(0.2, 0.7, 0.2, 1) var(--wire-confetti-delay) both; }
@keyframes wire-confetti-burst {
0% { opacity: 1; transform: translate(-50%, 0) rotate(0); }
100% { opacity: 0; transform: translate(calc(-50% + (var(--wire-confetti-x) - 50) * 0.9px), 9rem) rotate(540deg); }
}
@media (prefers-reduced-motion: reduce) {
.wire-confetti[data-running="true"] .wire-confetti__burst > i { animation-duration: 1ms; animation-delay: 0ms; }
}
}
}
-26
View File
@@ -1,26 +0,0 @@
component CopyMarkup {
outputs {
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Copy Markup"
name: string = ""
value: string = ""
placeholder: string = ""
type: string = "text"
min: string = ""
max: string = ""
step: string = ""
disabled: boolean = false
required: boolean = false
class: string = ""
}
view {
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--copy-markup {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
}
}
+88 -11
View File
@@ -1,26 +1,103 @@
component TreeView {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
expand(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
collapse(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { item: object; value: string; sourceEvent?: Event })
toggle(payload: { item: object; value: string; expanded: boolean; sourceEvent?: Event })
expand(payload: { item: object; value: string; sourceEvent?: Event })
collapse(payload: { item: object; value: string; sourceEvent?: Event })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Tree View"
description: string = ""
items: unknown[] = []
variant: string = "default"
valueKey: string = "value"
labelKey: string = "label"
defaultExpanded: unknown[] = []
selected: string = ""
size: string = "default"
color: string = "primary"
class: string = ""
}
state expandedValues = defaultExpanded
state selectedValue = selected
functions {
shared function nodeValue(item, index, parent) { return String(item[valueKey] || item.id || parent + index) }
shared function isExpanded(value) { return expandedValues.includes(value) }
client function toggleNode(item, value, sourceEvent, next, expanded) {
next = expandedValues.slice()
expanded = !next.includes(value)
if (expanded) {
next.push(value)
} else {
next = next.filter(function (entry) { return entry !== value })
}
expandedValues = next
output.toggle({ item: item, value: value, expanded: expanded, sourceEvent: sourceEvent })
if (expanded) {
output.expand({ item: item, value: value, sourceEvent: sourceEvent })
} else {
output.collapse({ item: item, value: value, sourceEvent: sourceEvent })
}
}
client function selectNode(item, value, sourceEvent) {
selectedValue = value
output.select({ item: item, value: value, sourceEvent: sourceEvent })
}
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--tree-view wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<section {...attrs} data-ui-component="TreeView" class='wire-tree-view {class}'>
{#if title}<strong class="wire-tree-view__title">{title}</strong>{/if}
{#if description}<p class="wire-tree-view__description">{description}</p>{/if}
<div class="wire-tree-view__tree" role="tree" aria-label='{title}' data-wrn-roving="vertical">
{#each items as item, index}
<div class="wire-tree-view__branch" role="none">
<div class="wire-tree-view__row" role="treeitem" aria-level="1" aria-expanded='{item.children && item.children.length ? isExpanded(nodeValue(item, index, "root-")) : ""}' aria-selected='{selectedValue === nodeValue(item, index, "root-") ? "true" : "false"}'>
{#if item.children && item.children.length}
<button type="button" class="wire-tree-view__toggle" aria-label='{isExpanded(nodeValue(item, index, "root-")) ? "Collapse " + item[labelKey] : "Expand " + item[labelKey]}' @click='toggleNode(item, nodeValue(item, index, "root-"), event)'><span class="icon-[lucide--chevron-right]" aria-hidden="true"></span></button>
{:else}<span class="wire-tree-view__toggle" aria-hidden="true"></span>{/if}
<button type="button" class="wire-tree-view__node" data-wrn-roving-item @click='selectNode(item, nodeValue(item, index, "root-"), event)'>
{#if item.icon}<span class='{item.icon}' aria-hidden="true"></span>{/if}<span>{item[labelKey]}</span>
</button>
</div>
{#if item.children && item.children.length}
<div class="wire-tree-view__group" role="group" data-show='isExpanded(nodeValue(item, index, "root-"))'>
{#each item.children as child, childIndex}
<div class="wire-tree-view__row" role="treeitem" aria-level="2" aria-selected='{selectedValue === nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-") ? "true" : "false"}'>
<span class="wire-tree-view__toggle" aria-hidden="true"></span>
<button type="button" class="wire-tree-view__node" data-wrn-roving-item @click='selectNode(child, nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-"), event)'>
{#if child.icon}<span class='{child.icon}' aria-hidden="true"></span>{/if}<span>{child[labelKey]}</span>
</button>
</div>
{/each}
</div>
{/if}
</div>
{:empty}<p class="wire-tree-view__empty">No tree items available.</p>{/each}
</div>
<slot />
</section>
}
style {
.wire-tree-view { display: grid; gap: 0.5rem; color: var(--wire-color-text); }
.wire-tree-view__title { font-size: 0.95rem; }
.wire-tree-view__description, .wire-tree-view__empty { margin: 0; color: var(--wire-color-muted); font-size: 0.8rem; }
.wire-tree-view__tree { display: grid; gap: 0.15rem; padding: 0.4rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); background: var(--wire-color-surface); }
.wire-tree-view__row { display: flex; min-width: 0; align-items: center; gap: 0.2rem; border-radius: var(--wire-radius-sm); }
.wire-tree-view__row[aria-selected="true"] { background: var(--wire-color-primary-soft); }
.wire-tree-view__toggle { display: inline-grid; width: 1.8rem; height: 1.8rem; flex: none; padding: 0; place-items: center; border: 0; border-radius: 0.35rem; color: var(--wire-color-muted); background: transparent; }
button.wire-tree-view__toggle { cursor: pointer; }
.wire-tree-view__toggle > span { width: 0.9rem; height: 0.9rem; transition: transform var(--wire-motion-fast); }
.wire-tree-view__row[aria-expanded="true"] > .wire-tree-view__toggle > span { transform: rotate(90deg); }
.wire-tree-view__node { display: flex; min-width: 0; flex: 1; align-items: center; gap: 0.45rem; padding: 0.45rem 0.5rem; border: 0; border-radius: var(--wire-radius-sm); color: inherit; background: transparent; font: inherit; text-align: left; cursor: pointer; }
.wire-tree-view__node:hover, button.wire-tree-view__toggle:hover { background: var(--wire-color-surface-2); }
.wire-tree-view__node:focus-visible, button.wire-tree-view__toggle:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: -2px; }
.wire-tree-view__node > [class*="icon-"] { width: 1rem; height: 1rem; flex: none; color: var(--wire-color-primary); }
.wire-tree-view__group { display: grid; margin-left: 1.8rem; padding-left: 0.45rem; border-left: 1px solid var(--wire-color-border); }
}
}
+63 -5
View File
@@ -30,11 +30,11 @@ test("global UI CSS stays below its migration ratchet", () => {
expect(gzipSync(new TextEncoder().encode(uiCss())).length).toBeLessThanOrEqual(12_672);
});
test("only the five scheduled component builds remain without local styles", () => {
test("every bundled component carries local styles", () => {
const unstyled = uiComponentNames().filter(
(name) => !/^\s{2,4}style \{/m.test(readFileSync(uiComponentPath(name), "utf8")),
);
expect(unstyled.sort()).toEqual(["Chart", "Clipboard", "Confetti", "CopyMarkup", "TreeView"]);
expect(unstyled).toEqual([]);
});
test("generated component reference documents every bundled component and its props", () => {
@@ -45,7 +45,7 @@ test("generated component reference documents every bundled component and its pr
readFileSync(join(uiComponentsDir(), "..", "component-catalog.json"), "utf8"),
) as { components: Array<{ name: string }> };
expect(reference.count).toBe(uiComponentNames().length);
expect(reference.count).toBe(103);
expect(reference.count).toBe(102);
expect(reference.components.map((component) => component.name)).toEqual(
expect.arrayContaining(uiComponentNames()),
);
@@ -146,7 +146,7 @@ test("removed duplicate aliases have valid canonical migration targets", () => {
const names = new Set(uiComponentNames());
expect(Object.keys(migration.replacements)).toHaveLength(migration.removedCount);
expect(migration.removedCount).toBe(5);
expect(migration.removedCount).toBe(6);
for (const [removed, replacement] of Object.entries(migration.replacements)) {
expect(names.has(removed)).toBe(false);
expect(names.has(replacement)).toBe(true);
@@ -3200,7 +3200,7 @@ test("no component gains an output that nothing ever emits", () => {
* A ceiling, not a target. It only ever moves down: rebuilding one of these
* components should tighten it.
*/
expect(offenders.length).toBeLessThanOrEqual(7);
expect(offenders.length).toBe(0);
expect(offenders).not.toContain("LayoutSplitter.sizeChange");
expect(offenders).not.toContain("CustomScrollbar.scroll");
});
@@ -3224,6 +3224,64 @@ test("list selects items without hrefs as well as navigation items", async () =>
expect(events).toEqual([{ item: { label: "Run report" }, index: 0 }]);
});
test("chart renders accessible SVG bars and emits point and legend outputs", async () => {
const source = readFileSync(uiComponentPath("Chart"), "utf8");
const dom = mountHtml(
await renderComponent(source, { items: [{ label: "Alpha", value: 12 }, { label: "Beta", value: 6 }] }),
);
const root = dom.querySelector('[data-ui-component="Chart"]') as HTMLElement;
const events: string[] = [];
for (const name of ["dataPointClick", "select", "legendToggle"]) {
root.addEventListener(name, () => events.push(name));
}
expect(root.querySelectorAll("svg rect")).toHaveLength(2);
(root.querySelector("svg rect") as SVGElement).dispatchEvent(
new (dom.window as { Event: typeof Event }).Event("click", { bubbles: true }),
);
(root.querySelector(".wire-chart__legend button") as HTMLButtonElement).click();
expect(events).toEqual(["dataPointClick", "select", "legendToggle"]);
});
test("tree view expands branches, selects nodes, and emits public outputs", async () => {
const source = readFileSync(uiComponentPath("TreeView"), "utf8");
const dom = mountHtml(
await renderComponent(source, {
items: [{ value: "docs", label: "Docs", children: [{ value: "guide", label: "Guide" }] }],
}),
);
const root = dom.querySelector('[data-ui-component="TreeView"]') as HTMLElement;
const events: string[] = [];
for (const name of ["toggle", "expand", "select"]) root.addEventListener(name, () => events.push(name));
(root.querySelector(".wire-tree-view__toggle") as HTMLButtonElement).click();
expect(root.querySelector(".wire-tree-view__group")?.getAttribute("data-show")).toBe("true");
(root.querySelector('[aria-level="2"] .wire-tree-view__node') as HTMLButtonElement).click();
expect(events).toEqual(["toggle", "expand", "select"]);
});
test("confetti completes deferred bursts and clipboard reports successful copies", async () => {
const confetti = mountHtml(await renderComponent(readFileSync(uiComponentPath("Confetti"), "utf8"), { duration: 5, count: 4 }));
const confettiRoot = confetti.querySelector('[data-ui-component="Confetti"]') as HTMLElement;
const confettiEvents: string[] = [];
confettiRoot.addEventListener("start", () => confettiEvents.push("start"));
confettiRoot.addEventListener("complete", () => confettiEvents.push("complete"));
(confettiRoot.querySelector("button") as HTMLButtonElement).click();
await new Promise((resolve) => setTimeout(resolve, 15));
expect(confettiEvents).toEqual(["start", "complete"]);
const clipboard = mountHtml(await renderComponent(readFileSync(uiComponentPath("Clipboard"), "utf8"), { value: "npm add wrnexus" }));
Object.defineProperty((clipboard.window as { navigator: object }).navigator, "clipboard", {
configurable: true,
value: { writeText: () => Promise.resolve() },
});
const clipboardRoot = clipboard.querySelector('[data-ui-component="Clipboard"]') as HTMLElement;
const clipboardEvents: string[] = [];
for (const name of ["copy", "success"]) clipboardRoot.addEventListener(name, () => clipboardEvents.push(name));
(clipboardRoot.querySelector("button") as HTMLButtonElement).click();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(clipboardEvents).toEqual(["copy", "success"]);
expect(clipboardRoot.dataset.status).toBe("success");
});
test("a declared output is never emitted as a hand-built CustomEvent", () => {
/*
* The failure this prevents is silent in both directions: the component