diff --git a/docs/ui-visual-contract-0.8.json b/docs/ui-visual-contract-0.8.json index 05010b86..df205aed 100644 --- a/docs/ui-visual-contract-0.8.json +++ b/docs/ui-visual-contract-0.8.json @@ -15,17 +15,16 @@ "packages/ui/components/CTASection.wrn": "2d2241d4f1018cfd2f8ea1ddd5fa8c8fcb8d1bbb2d4e4498cb9f39ff47a3801c", "packages/ui/components/Card.wrn": "659fbc12e446e87043d6bf62f51efbc05b451bdfc7210ede041f68a991dadf30", "packages/ui/components/Carousel.wrn": "e599710161b4c4fd67a0bb45b5621a82b000362951e7a9e9153d55c8a8cc1dfd", - "packages/ui/components/Chart.wrn": "841def6b024720bd24898975a1434a1cb39c89e1272d42db431d35d27c98c553", + "packages/ui/components/Chart.wrn": "d2cedd0e02818d0908708c2d07a495c629fad13fa021a2e035d5023dc5c367de", "packages/ui/components/ChatBubble.wrn": "ddc0e63a055d59535002eeba5b3527c51fb0989d62bffba2f53dceedfeb771dc", - "packages/ui/components/Clipboard.wrn": "bb8651c7e44517db827603d0c0e546b6891093d7e6589c0b6e45d91b6187591b", + "packages/ui/components/Clipboard.wrn": "03fcfb9786008fd7fb0a7719c3279711253afc5bbb9b0262b805dd553c44da8a", "packages/ui/components/Collapse.wrn": "5205eefaf96d9a4c143779ce9f6329537b1c807eff47dbcf62fcc9bcdefaef06", "packages/ui/components/ColorPicker.wrn": "af0f962555b581df75a6dffb3056d394708c29d93ad18afa86e31707b505a807", "packages/ui/components/Columns.wrn": "e01496aa53025444cc51671efcce40d6d24d86775b2cbb1ee0def1862bb6315d", "packages/ui/components/Combobox.wrn": "b6ae8cf667ed98f3fc4dc141bde797daf7e3daacf7df6c9a7c25a3a143432f62", - "packages/ui/components/Confetti.wrn": "22d687beefd5047055f65e806b4e39f81e18887532ec18d439ac364a044b230a", + "packages/ui/components/Confetti.wrn": "b9aece64735c721c24356122e52ca84602d9f646c7d8be1e0b80d9de3163a501", "packages/ui/components/Container.wrn": "4a0acbb896f4c11714e3925d912b10ef9aeedfa904b4ad8561aa2cbd9a0fdca0", "packages/ui/components/ContextMenu.wrn": "33fb0f60268b12411d66e3031dc0557cb231149e7a8d83340b4b453a6e37cb09", - "packages/ui/components/CopyMarkup.wrn": "8d57a7d72e02c126f855a181fc36e1b969edb90f683dce6b6ffe546d49d2b29b", "packages/ui/components/CustomScrollbar.wrn": "fedb2ab06cc2cf9499c130590945ca6971a3edfb60407b6658ef7756e4be9823", "packages/ui/components/DataMap.wrn": "7d0384d89126d947a6c2d640c93ab047aecca7931569d11b54d82dbb6639ff0e", "packages/ui/components/DataTable.wrn": "4e4562b0e18748f062a8b20153767aaaf62c33d98542ff14af4e57da7d8c2469", @@ -88,7 +87,7 @@ "packages/ui/components/Toaster.wrn": "288b2b370f65bf823e8d2a6f8c8cf66972332b8174303d33ebe8546d5e1d52c4", "packages/ui/components/ToggleCount.wrn": "ee706b191fcfc01dd7bde0ddba1940af400ed2d01b639016134699963b1000ab", "packages/ui/components/TogglePassword.wrn": "c644436c3435b8a9b39c6b7a9f75671b76ca74f273952ce5c032ca169b0470e5", - "packages/ui/components/TreeView.wrn": "1791a10135595b17b5c746af9e0476e8520d516ba46d64c79dc4efe0af2b500d", + "packages/ui/components/TreeView.wrn": "c354fce9c5e81fed6eec4089dfd2a7f6bb84eb189c94efc88e00251efdc12c0b", "packages/ui/components/Typography.wrn": "1b4d1b91dee0ff522d27c566becc3bc49bd9ec979ae6334e6a15fca5d88eccac", "packages/ui/components/WysiwygEditor.wrn": "e4d341f7f49b53301d790bcebe7f3788fea4ea4b72ba8bf0a422e006ceeb54eb", "packages/ui/components/alert.wrn": "2eee24aeaa345fce462439c9286dce737716218c783c3a136e55483b8d925ffd", diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index df41a378..58895955 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -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, ); diff --git a/packages/ui/COMPONENTS.md b/packages/ui/COMPONENTS.md index 34753222..d91b3fb7 100644 --- a/packages/ui/COMPONENTS.md +++ b/packages/ui/COMPONENTS.md @@ -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; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array; 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; 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; 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 diff --git a/packages/ui/component-catalog.json b/packages/ui/component-catalog.json index df305bdc..34921242 100644 --- a/packages/ui/component-catalog.json +++ b/packages/ui/component-catalog.json @@ -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", diff --git a/packages/ui/component-migrations.json b/packages/ui/component-migrations.json index d2b59075..0b84b0e1 100644 --- a/packages/ui/component-migrations.json +++ b/packages/ui/component-migrations.json @@ -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" } } diff --git a/packages/ui/component-reference.json b/packages/ui/component-reference.json index 0950ef71..7a569166 100644 --- a/packages/ui/component-reference.json +++ b/packages/ui/component-reference.json @@ -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; 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; 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": [ diff --git a/packages/ui/components/Chart.wrn b/packages/ui/components/Chart.wrn index 3f8128a3..bc00de5e 100644 --- a/packages/ui/components/Chart.wrn +++ b/packages/ui/components/Chart.wrn @@ -1,25 +1,107 @@ component Chart { outputs { - select(payload: { value?: string | number | boolean | null; values?: Array; 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 { -
- {#if title}{title}{/if} - {#if description}

{description}

{/if} - {#if items}
{#each items as item}{item.label}{/each}
{/if} +
+ {#if title || description} +
+ {#if title}{title}{/if} + {#if description}{description}{/if} +
+ {/if} + + + {#each items as item, index} + + + {#if showValues}{numericValue(item)}{/if} + {item[labelKey]} + + {/each} + + {#if showLegend} +
+ {#each items as item, index} + + {/each} +
+ {/if} -
+ + } + + 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; } } } diff --git a/packages/ui/components/Clipboard.wrn b/packages/ui/components/Clipboard.wrn index 402f7db9..ef214e26 100644 --- a/packages/ui/components/Clipboard.wrn +++ b/packages/ui/components/Clipboard.wrn @@ -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 { -
- {#if title}{title}{/if} - {#if description}

{description}

{/if} - {#if items}
{#each items as item}{item.label}{/each}
{/if} +
+ {#if title}{title}{/if} + {#if description}

{description}

{/if} +
+ {#if code} + {value} + {:else} + {value} + {/if} + +
} + + 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; } + } } diff --git a/packages/ui/components/Confetti.wrn b/packages/ui/components/Confetti.wrn index e53158cd..dea7e97e 100644 --- a/packages/ui/components/Confetti.wrn +++ b/packages/ui/components/Confetti.wrn @@ -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 { -
- {#if title}{title}{/if} - {#if description}

{description}

{/if} - {#if items}
{#each items as item}{item.label}{/each}
{/if} +
+ + -
+ + } + + 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; } + } } } diff --git a/packages/ui/components/CopyMarkup.wrn b/packages/ui/components/CopyMarkup.wrn deleted file mode 100644 index 7607d603..00000000 --- a/packages/ui/components/CopyMarkup.wrn +++ /dev/null @@ -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 { - - } -} diff --git a/packages/ui/components/TreeView.wrn b/packages/ui/components/TreeView.wrn index 5f6a99d6..8bd9f6ea 100644 --- a/packages/ui/components/TreeView.wrn +++ b/packages/ui/components/TreeView.wrn @@ -1,26 +1,103 @@ component TreeView { outputs { - select(payload: { value?: string | number | boolean | null; values?: Array; 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 { -
- {#if title}{title}{/if} - {#if description}

{description}

{/if} - {#if items}
{#each items as item}{item.label}{/each}
{/if} +
+ {#if title}{title}{/if} + {#if description}

{description}

{/if} +
+ {#each items as item, index} +
+
+ {#if item.children && item.children.length} + + {:else}{/if} + +
+ {#if item.children && item.children.length} +
+ {#each item.children as child, childIndex} +
+ + +
+ {/each} +
+ {/if} +
+ {:empty}

No tree items available.

{/each} +
} + + 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); } + } } diff --git a/packages/ui/test/ui.test.ts b/packages/ui/test/ui.test.ts index 8d8afbed..facaf20f 100644 --- a/packages/ui/test/ui.test.ts +++ b/packages/ui/test/ui.test.ts @@ -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