chore(release): prepare 0.8.6
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
Quality / quality (windows-latest) (push) Canceled after 0s

Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6,
and rebuilds the editor compiler, language server and extension bundles that
embed the version.

The release carries the output delivery fix: camelCase outputs now reach
parent bindings, and 18 components emit through output.* instead of
hand-built CustomEvents. See the 0.8.6 migration entry for what changes for
consumers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 01:54:44 +05:30
co-authored by Claude Opus 5
parent 5e65627305
commit 7a2b58652a
117 changed files with 4467 additions and 2907 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT",
@@ -41,8 +41,8 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/db": "^0.8.5"
"@wrnexus/core": "^0.8.6",
"@wrnexus/db": "^0.8.6"
},
"files": [
"dist",
+18 -18
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT",
@@ -44,23 +44,23 @@
"wrnexus": "./dist/index.js"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/router": "^0.8.5",
"@wrnexus/csr": "^0.8.5",
"@wrnexus/compiler": "^0.8.5",
"@wrnexus/styles": "^0.8.5",
"@wrnexus/dev-server": "^0.8.5",
"@wrnexus/ui": "^0.8.5",
"@wrnexus/validation": "^0.8.5",
"@wrnexus/i18n": "^0.8.5",
"@wrnexus/mcp": "^0.8.5",
"@wrnexus/playground": "^0.8.5",
"@wrnexus/db": "^0.8.5",
"@wrnexus/authz": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/syntax": "^0.8.5",
"@wrnexus/typecheck": "^0.8.5",
"@wrnexus/security": "^0.8.5",
"@wrnexus/core": "^0.8.6",
"@wrnexus/router": "^0.8.6",
"@wrnexus/csr": "^0.8.6",
"@wrnexus/compiler": "^0.8.6",
"@wrnexus/styles": "^0.8.6",
"@wrnexus/dev-server": "^0.8.6",
"@wrnexus/ui": "^0.8.6",
"@wrnexus/validation": "^0.8.6",
"@wrnexus/i18n": "^0.8.6",
"@wrnexus/mcp": "^0.8.6",
"@wrnexus/playground": "^0.8.6",
"@wrnexus/db": "^0.8.6",
"@wrnexus/authz": "^0.8.6",
"@wrnexus/plugin": "^0.8.6",
"@wrnexus/syntax": "^0.8.6",
"@wrnexus/typecheck": "^0.8.6",
"@wrnexus/security": "^0.8.6",
"selfsigned": "^5.5.0"
},
"files": [
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT",
@@ -37,10 +37,10 @@
}
},
"dependencies": {
"@wrnexus/csr": "^0.8.5",
"@wrnexus/syntax": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/validation": "^0.8.5"
"@wrnexus/csr": "^0.8.6",
"@wrnexus/syntax": "^0.8.6",
"@wrnexus/store": "^0.8.6",
"@wrnexus/validation": "^0.8.6"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
"@wrnexus/core": "^0.8.6"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
"license": "MIT",
+22 -22
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT",
@@ -41,27 +41,27 @@
}
},
"dependencies": {
"@wrnexus/authz": "^0.8.5",
"@wrnexus/rpc": "^0.8.5",
"@wrnexus/core": "^0.8.5",
"@wrnexus/dev-toolbar": "^0.8.5",
"@wrnexus/router": "^0.8.5",
"@wrnexus/ssr": "^0.8.5",
"@wrnexus/csr": "^0.8.5",
"@wrnexus/compiler": "^0.8.5",
"@wrnexus/styles": "^0.8.5",
"@wrnexus/ui": "^0.8.5",
"@wrnexus/validation": "^0.8.5",
"@wrnexus/i18n": "^0.8.5",
"@wrnexus/db": "^0.8.5",
"@wrnexus/pubsub": "^0.8.5",
"@wrnexus/uploader": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/security": "^0.8.5",
"@wrnexus/observability": "^0.8.5",
"@wrnexus/cache": "^0.8.5",
"@wrnexus/pwa": "^0.8.5"
"@wrnexus/authz": "^0.8.6",
"@wrnexus/rpc": "^0.8.6",
"@wrnexus/core": "^0.8.6",
"@wrnexus/dev-toolbar": "^0.8.6",
"@wrnexus/router": "^0.8.6",
"@wrnexus/ssr": "^0.8.6",
"@wrnexus/csr": "^0.8.6",
"@wrnexus/compiler": "^0.8.6",
"@wrnexus/styles": "^0.8.6",
"@wrnexus/ui": "^0.8.6",
"@wrnexus/validation": "^0.8.6",
"@wrnexus/i18n": "^0.8.6",
"@wrnexus/db": "^0.8.6",
"@wrnexus/pubsub": "^0.8.6",
"@wrnexus/uploader": "^0.8.6",
"@wrnexus/plugin": "^0.8.6",
"@wrnexus/store": "^0.8.6",
"@wrnexus/security": "^0.8.6",
"@wrnexus/observability": "^0.8.6",
"@wrnexus/cache": "^0.8.6",
"@wrnexus/pwa": "^0.8.6"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
"@wrnexus/core": "^0.8.6"
},
"files": [
"dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
"@wrnexus/core": "^0.8.6"
},
"files": [
"dist",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
"license": "MIT",
@@ -42,9 +42,9 @@
"./components/*": "./components/*"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
"@wrnexus/core": "^0.8.6",
"@wrnexus/plugin": "^0.8.6",
"@wrnexus/ui": "^0.8.6"
},
"wrnexus": {
"plugin": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
"@wrnexus/core": "^0.8.6"
},
"files": [
"dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/native": "^0.8.5"
"@wrnexus/native": "^0.8.6"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/jwt": "^0.8.5"
"@wrnexus/jwt": "^0.8.6"
},
"files": [
"dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT",
@@ -49,7 +49,7 @@
}
},
"dependencies": {
"@wrnexus/syntax": "^0.8.5"
"@wrnexus/syntax": "^0.8.6"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
"@wrnexus/core": "^0.8.6"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT",
@@ -37,8 +37,8 @@
}
},
"dependencies": {
"@wrnexus/compiler": "^0.8.5",
"@wrnexus/core": "^0.8.5"
"@wrnexus/compiler": "^0.8.6",
"@wrnexus/core": "^0.8.6"
},
"files": [
"dist",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT",
@@ -45,9 +45,9 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/security": "^0.8.5"
"@wrnexus/core": "^0.8.6",
"@wrnexus/store": "^0.8.6",
"@wrnexus/security": "^0.8.6"
},
"files": [
"dist",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT",
@@ -37,9 +37,9 @@
}
},
"dependencies": {
"@wrnexus/uploader": "^0.8.5",
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5"
"@wrnexus/uploader": "^0.8.6",
"@wrnexus/core": "^0.8.6",
"@wrnexus/plugin": "^0.8.6"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/syntax — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT",
+33 -33
View File
@@ -229,7 +229,7 @@ Present structured responsive linked or status items with icons, descriptions, a
- Mount: `data-component="List"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ListGroup
@@ -247,7 +247,7 @@ Continuously present responsive labels, partners, notices, or capabilities with
- Mount: `data-component="Marquee"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
- Outputs: `pause({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `resume({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Progress
@@ -301,7 +301,7 @@ Present responsive chronological activity, milestones, or workflow status with r
- Mount: `data-component="Timeline"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Toast
@@ -469,7 +469,7 @@ Provide an accessible responsive search field with labels, validation states, si
- Mount: `data-component="SearchBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: None
- Outputs: `search({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Select
@@ -588,7 +588,7 @@ Present responsive location information and markers with map-ready metadata and
- Mount: `data-component="Map"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
- Outputs: `markerClick({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `zoom({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ToastNotifications
@@ -624,7 +624,7 @@ Create responsive balanced content columns with configurable count, gap, density
Constrain and align page content with responsive gutters and compact, wide, or full width options.
- Mount: `data-component="Container"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `centered: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -633,16 +633,16 @@ Constrain and align page content with responsive gutters and compact, wide, or f
Theme-aware, responsive custom scrollbar component.
- Mount: `data-component="CustomScrollbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `axis: string = "vertical"`, `thickness: number = 8`, `maxHeight: string = "20rem"`, `radius: string = "999px"`, `class: string = ""`
- Slots: `default`
- Outputs: `scroll({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
- Outputs: None
### Divider
Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.
- Mount: `data-component="Divider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `variant: string = "solid"`, `class: string = ""`
- Slots: None
- Outputs: None
@@ -669,7 +669,7 @@ Render structured responsive footer navigation, pre and post content, copyright
Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.
- Mount: `data-component="Grid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `minItemWidth: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -678,7 +678,7 @@ Arrange arbitrary content in a responsive configurable CSS grid with stable colu
Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.
- Mount: `data-component="Image"`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `rounded: boolean = false`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `fit: string = "cover"`, `rounded: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -687,7 +687,7 @@ Render a responsive image with explicit dimensions, loading behavior, alternativ
Theme-aware, responsive kbd component.
- Mount: `data-component="Kbd"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -696,16 +696,16 @@ Theme-aware, responsive kbd component.
Theme-aware, responsive layout splitter component.
- Mount: `data-component="LayoutSplitter"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: `resizeStart({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resize({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resizeEnd({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
- Props: `color: string = "primary"`, `size: number = 50`, `orientation: string = "horizontal"`, `minSize: number = 15`, `step: number = 5`, `label: string = "Resize panels"`, `class: string = ""`
- Slots: `start`, `end`, `default`
- Outputs: `sizeChange({ size: number })`
### Link
Render an accessible internal or external link with target, relation, size, color, and public focus or click events.
- Mount: `data-component="Link"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `underline: string = "hover"`, `external: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -862,18 +862,18 @@ Show responsive hierarchical navigation with home support, separators, current-p
Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Mega Menu"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `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)`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
### Nav
Theme-aware, responsive nav component.
- Mount: `data-component="Nav"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Nav"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `label: string = "Main"`, `collapsible: boolean = true`, `toggleLabel: string = "Menu"`, `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)`, `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)`
- Outputs: `select({ item: object; value: string })`
### Navbar
@@ -889,45 +889,45 @@ Theme-aware, responsive navbar component.
Theme-aware, responsive pagination component.
- Mount: `data-component="Pagination"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Pagination"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `page: number = 1`, `pageSize: number = 10`, `total: number = 0`, `variant: string = "compact"`, `siblingCount: number = 1`, `showSummary: boolean = true`, `label: string = "Pagination"`, `previousLabel: string = "Previous"`, `nextLabel: string = "Next"`, `class: string = ""`
- Slots: `default`
- Outputs: `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)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
- Outputs: `change({ page: number; pageSize: number })`, `previous({ page: number })`, `next({ page: number })`
### Scrollspy
Theme-aware, responsive scrollspy component.
- Mount: `data-component="Scrollspy"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Scrollspy"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `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)`
- Outputs: `change({ href: string; label: string })`
### Sidebar
Theme-aware, responsive sidebar component.
- Mount: `data-component="Sidebar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mobileLabel: string = "Open navigation"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ source: string })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `mobileLabel: string = "Open navigation"`, `drawerTitle: string = "Navigation"`, `class: string = ""`
- Slots: `default`, `drawer`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent: Event })`, `close({ source: string })`, `select({ item: object; value: string; level: number })`
### Stepper
Theme-aware, responsive stepper component.
- Mount: `data-component="Stepper"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Stepper"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `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)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
- Props: `color: string = "primary"`, `size: string = "default"`, `steps: unknown[] = []`, `active: number = 0`, `orientation: string = "horizontal"`, `clickable: boolean = false`, `label: string = "Progress"`, `showPanel: boolean = false`, `controls: boolean = false`, `allowSkip: boolean = false`, `nextDisabled: boolean = false`, `backLabel: string = "Back"`, `nextLabel: string = "Next"`, `skipLabel: string = "Skip"`, `finishLabel: string = "Finish"`, `class: string = ""`
- Slots: `step-{index}`, `panel-{index}`, `default`
- Outputs: `change({ index: number; step: object })`, `back({ index: number; step: object })`, `next({ index: number; step: object })`, `skip({ index: number; step: object })`, `finish({ index: number; step: object })`
### Tabs
Switch between related responsive content panels with horizontal or vertical orientation and selection events.
- Mount: `data-component="Tabs"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Tabs"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: None
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mode: string = "client"`, `param: string = "tab"`, `label: string = "Tabs"`, `class: string = ""`
- Slots: `panel-{valueOf(item, index)}`, `default`
- Outputs: `change({ value: string; item: object; index: number })`, `select({ value: string; item: object; index: number })`
## Overlays
+389 -170
View File
@@ -4153,6 +4153,13 @@
"default": "\"xl\"",
"options": []
},
{
"name": "centered",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "class",
"type": "string",
@@ -4443,13 +4450,6 @@
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -4458,24 +4458,38 @@
"options": []
},
{
"name": "columns",
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "axis",
"type": "string",
"required": false,
"default": "\"vertical\"",
"options": []
},
{
"name": "thickness",
"type": "number",
"required": false,
"default": "2",
"default": "8",
"options": []
},
{
"name": "gap",
"name": "maxHeight",
"type": "string",
"required": false,
"default": "\"md\"",
"default": "\"20rem\"",
"options": []
},
{
"name": "maxWidth",
"name": "radius",
"type": "string",
"required": false,
"default": "\"xl\"",
"default": "\"999px\"",
"options": []
},
{
@@ -4487,13 +4501,8 @@
}
],
"slots": ["default"],
"outputs": [
{
"name": "scroll",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
}
],
"events": ["scroll"],
"outputs": [],
"events": [],
"source": "components/CustomScrollbar.wrn"
},
{
@@ -5192,6 +5201,13 @@
"default": "\"horizontal\"",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"solid\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -6548,6 +6564,13 @@
"default": "\"xl\"",
"options": []
},
{
"name": "minItemWidth",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -6962,6 +6985,13 @@
"default": "\"lazy\"",
"options": []
},
{
"name": "fit",
"type": "string",
"required": false,
"default": "\"cover\"",
"options": []
},
{
"name": "rounded",
"type": "boolean",
@@ -7750,7 +7780,7 @@
"name": "label",
"type": "string",
"required": false,
"default": "\"K\"",
"default": "\"K\"",
"options": []
},
{
@@ -7772,13 +7802,6 @@
"category": "layout",
"purpose": "Theme-aware, responsive layout splitter component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -7787,24 +7810,38 @@
"options": []
},
{
"name": "columns",
"name": "size",
"type": "number",
"required": false,
"default": "2",
"default": "50",
"options": []
},
{
"name": "gap",
"name": "orientation",
"type": "string",
"required": false,
"default": "\"md\"",
"default": "\"horizontal\"",
"options": []
},
{
"name": "maxWidth",
"name": "minSize",
"type": "number",
"required": false,
"default": "15",
"options": []
},
{
"name": "step",
"type": "number",
"required": false,
"default": "5",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"xl\"",
"default": "\"Resize panels\"",
"options": []
},
{
@@ -7815,22 +7852,14 @@
"options": []
}
],
"slots": ["default"],
"slots": ["start", "end", "default"],
"outputs": [
{
"name": "resizeStart",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
},
{
"name": "resize",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
},
{
"name": "resizeEnd",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
"name": "sizeChange",
"payloadType": "{ size: number }"
}
],
"events": ["resizeStart", "resize", "resizeEnd"],
"events": ["sizeChange"],
"source": "components/LayoutSplitter.wrn"
},
{
@@ -7951,6 +7980,13 @@
"default": "\"\"",
"options": []
},
{
"name": "underline",
"type": "string",
"required": false,
"default": "\"hover\"",
"options": []
},
{
"name": "external",
"type": "boolean",
@@ -8028,8 +8064,13 @@
}
],
"slots": ["default"],
"outputs": [],
"events": [],
"outputs": [
{
"name": "select",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
}
],
"events": ["select"],
"source": "components/List.wrn"
},
{
@@ -8159,8 +8200,21 @@
}
],
"slots": ["default"],
"outputs": [],
"events": [],
"outputs": [
{
"name": "markerClick",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
},
{
"name": "select",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
},
{
"name": "zoom",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
}
],
"events": ["markerClick", "select", "zoom"],
"source": "components/Map.wrn"
},
{
@@ -8316,8 +8370,17 @@
}
],
"slots": ["default"],
"outputs": [],
"events": [],
"outputs": [
{
"name": "pause",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
},
{
"name": "resume",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
}
],
"events": ["pause", "resume"],
"source": "components/Marquee.wrn"
},
{
@@ -8326,13 +8389,6 @@
"category": "navigation",
"purpose": "Theme-aware, responsive mega menu component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -8340,32 +8396,46 @@
"default": "\"primary\"",
"options": []
},
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Mega Menu\"",
"default": "\"Menu\"",
"options": []
},
{
"name": "items",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "active",
"name": "icon",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "orientation",
"name": "columns",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "footer",
"type": "string",
"required": false,
"default": "\"horizontal\"",
"default": "\"\"",
"options": []
},
{
"name": "defaultOpen",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
@@ -8380,15 +8450,15 @@
"outputs": [
{
"name": "open",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ sourceEvent: Event }"
},
{
"name": "close",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ reason: string }"
},
{
"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 }"
}
],
"events": ["open", "close", "select"],
@@ -8961,13 +9031,6 @@
"category": "navigation",
"purpose": "Theme-aware, responsive nav component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -8976,10 +9039,10 @@
"options": []
},
{
"name": "label",
"name": "size",
"type": "string",
"required": false,
"default": "\"Nav\"",
"default": "\"default\"",
"options": []
},
{
@@ -9003,6 +9066,27 @@
"default": "\"horizontal\"",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Main\"",
"options": []
},
{
"name": "collapsible",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "toggleLabel",
"type": "string",
"required": false,
"default": "\"Menu\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -9015,14 +9099,10 @@
"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"
},
{
"name": "change",
"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 }"
}
],
"events": ["select", "change"],
"events": ["select"],
"source": "components/Nav.wrn"
},
{
@@ -9342,6 +9422,13 @@
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component.",
"props": [
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "size",
"type": "string",
@@ -9350,10 +9437,45 @@
"options": []
},
{
"name": "color",
"name": "page",
"type": "number",
"required": false,
"default": "1",
"options": []
},
{
"name": "pageSize",
"type": "number",
"required": false,
"default": "10",
"options": []
},
{
"name": "total",
"type": "number",
"required": false,
"default": "0",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"primary\"",
"default": "\"compact\"",
"options": []
},
{
"name": "siblingCount",
"type": "number",
"required": false,
"default": "1",
"options": []
},
{
"name": "showSummary",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
@@ -9364,24 +9486,17 @@
"options": []
},
{
"name": "items",
"type": "unknown[]",
"name": "previousLabel",
"type": "string",
"required": false,
"default": "[]",
"default": "\"Previous\"",
"options": []
},
{
"name": "active",
"name": "nextLabel",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "orientation",
"type": "string",
"required": false,
"default": "\"horizontal\"",
"default": "\"Next\"",
"options": []
},
{
@@ -9396,15 +9511,15 @@
"outputs": [
{
"name": "change",
"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": "{ page: number; pageSize: number }"
},
{
"name": "previous",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ page: number }"
},
{
"name": "next",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ page: number }"
}
],
"events": ["change", "previous", "next"],
@@ -10664,13 +10779,6 @@
"category": "navigation",
"purpose": "Theme-aware, responsive scrollspy component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -10679,10 +10787,10 @@
"options": []
},
{
"name": "label",
"name": "size",
"type": "string",
"required": false,
"default": "\"Scrollspy\"",
"default": "\"default\"",
"options": []
},
{
@@ -10700,10 +10808,17 @@
"options": []
},
{
"name": "orientation",
"name": "label",
"type": "string",
"required": false,
"default": "\"horizontal\"",
"default": "\"On this page\"",
"options": []
},
{
"name": "heading",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
@@ -10718,7 +10833,7 @@
"outputs": [
{
"name": "change",
"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": "{ href: string; label: string }"
}
],
"events": ["change"],
@@ -10823,8 +10938,17 @@
}
],
"slots": [],
"outputs": [],
"events": [],
"outputs": [
{
"name": "search",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
},
{
"name": "clear",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
}
],
"events": ["search", "clear"],
"source": "components/SearchBox.wrn"
},
{
@@ -11192,13 +11316,6 @@
"category": "navigation",
"purpose": "Theme-aware, responsive sidebar component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -11206,6 +11323,13 @@
"default": "\"primary\"",
"options": []
},
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "label",
"type": "string",
@@ -11227,13 +11351,6 @@
"default": "\"\"",
"options": []
},
{
"name": "orientation",
"type": "string",
"required": false,
"default": "\"horizontal\"",
"options": []
},
{
"name": "mobileLabel",
"type": "string",
@@ -11241,6 +11358,13 @@
"default": "\"Open navigation\"",
"options": []
},
{
"name": "drawerTitle",
"type": "string",
"required": false,
"default": "\"Navigation\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -11249,7 +11373,7 @@
"options": []
}
],
"slots": ["default"],
"slots": ["default", "drawer"],
"outputs": [
{
"name": "toggle",
@@ -11257,7 +11381,7 @@
},
{
"name": "open",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ sourceEvent: Event }"
},
{
"name": "close",
@@ -11265,7 +11389,7 @@
},
{
"name": "select",
"payloadType": "{ item: string | number | boolean | null | object; value: string | number | boolean; level: string }"
"payloadType": "{ item: object; value: string; level: number }"
}
],
"events": ["toggle", "open", "close", "select"],
@@ -11682,13 +11806,6 @@
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -11697,14 +11814,14 @@
"options": []
},
{
"name": "label",
"name": "size",
"type": "string",
"required": false,
"default": "\"Stepper\"",
"default": "\"default\"",
"options": []
},
{
"name": "items",
"name": "steps",
"type": "unknown[]",
"required": false,
"default": "[]",
@@ -11712,9 +11829,9 @@
},
{
"name": "active",
"type": "string",
"type": "number",
"required": false,
"default": "\"\"",
"default": "0",
"options": []
},
{
@@ -11724,6 +11841,76 @@
"default": "\"horizontal\"",
"options": []
},
{
"name": "clickable",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Progress\"",
"options": []
},
{
"name": "showPanel",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "controls",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "allowSkip",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "nextDisabled",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "backLabel",
"type": "string",
"required": false,
"default": "\"Back\"",
"options": []
},
{
"name": "nextLabel",
"type": "string",
"required": false,
"default": "\"Next\"",
"options": []
},
{
"name": "skipLabel",
"type": "string",
"required": false,
"default": "\"Skip\"",
"options": []
},
{
"name": "finishLabel",
"type": "string",
"required": false,
"default": "\"Finish\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -11732,26 +11919,30 @@
"options": []
}
],
"slots": ["default"],
"slots": ["step-{index}", "panel-{index}", "default"],
"outputs": [
{
"name": "change",
"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": "{ index: number; step: object }"
},
{
"name": "previous",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"name": "back",
"payloadType": "{ index: number; step: object }"
},
{
"name": "next",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ index: number; step: object }"
},
{
"name": "complete",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"name": "skip",
"payloadType": "{ index: number; step: object }"
},
{
"name": "finish",
"payloadType": "{ index: number; step: object }"
}
],
"events": ["change", "previous", "next", "complete"],
"events": ["change", "back", "next", "skip", "finish"],
"source": "components/Stepper.wrn"
},
{
@@ -12204,13 +12395,6 @@
"category": "navigation",
"purpose": "Switch between related responsive content panels with horizontal or vertical orientation and selection events.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -12219,10 +12403,10 @@
"options": []
},
{
"name": "label",
"name": "size",
"type": "string",
"required": false,
"default": "\"Tabs\"",
"default": "\"default\"",
"options": []
},
{
@@ -12246,6 +12430,27 @@
"default": "\"horizontal\"",
"options": []
},
{
"name": "mode",
"type": "string",
"required": false,
"default": "\"client\"",
"options": []
},
{
"name": "param",
"type": "string",
"required": false,
"default": "\"tab\"",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Tabs\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -12254,9 +12459,18 @@
"options": []
}
],
"slots": ["default"],
"outputs": [],
"events": [],
"slots": ["panel-{valueOf(item, index)}", "default"],
"outputs": [
{
"name": "change",
"payloadType": "{ value: string; item: object; index: number }"
},
{
"name": "select",
"payloadType": "{ value: string; item: object; index: number }"
}
],
"events": ["change", "select"],
"source": "components/Tabs.wrn"
},
{
@@ -12856,8 +13070,13 @@
}
],
"slots": ["default"],
"outputs": [],
"events": [],
"outputs": [
{
"name": "select",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
}
],
"events": ["select"],
"source": "components/Timeline.wrn"
},
{
+13 -11
View File
@@ -49,22 +49,24 @@ component Accordion {
return multiple || alwaysOpen
}
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-accordion]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent(eventName, true, false, {
// Named rather than computed: an output is resolved as a property name, so
// output[eventName] would not reach a parent binding.
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, payload) {
payload = {
component: "Accordion",
value: value,
item: item,
open: isOpen(value),
openValues: openValues
})
root.dispatchEvent(customEvent)
}
if (eventName === "open") {
output.open(payload)
} else if (eventName === "close") {
output.close(payload)
} else {
output.change(payload)
}
}
client function toggleItem(sourceEvent, value, item, wasOpen) {
+2 -6
View File
@@ -30,17 +30,13 @@ component AvatarGroup {
return items.slice(Number(maxVisible))
}
client function toggleOverflow(sourceEvent, root, customEvent) {
client function toggleOverflow() {
overflowOpen = !overflowOpen
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("overflow", true, false, {
output.overflow({
component: "AvatarGroup",
open: overflowOpen,
hiddenCount: hiddenMembers().length
})
root.dispatchEvent(customEvent)
}
}
+3 -3
View File
@@ -34,7 +34,7 @@ label: string = "Breadcrumb"
<a
href='{homeHref || "/"}'
class="wire-breadcrumb__link wire-breadcrumb__home"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 } }))'
@click='output.select({ item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 })'
>
{#if homeIcon === "icon-[lucide--house]"}
<span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
@@ -72,7 +72,7 @@ label: string = "Breadcrumb"
target='{item.target || ""}'
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
class="wire-breadcrumb__link"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
@click='output.select({ item: item, itemIndex: itemIndex })'
>
{#if item.icon}
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
@@ -87,7 +87,7 @@ label: string = "Breadcrumb"
{:else}
<span
class="wire-breadcrumb__current"
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : ""}'
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
>
{#if item.icon}
+88 -14
View File
@@ -1,3 +1,13 @@
// Columns -- a simple multi-column split for page content.
//
// <Columns columns={2} gap="lg">...</Columns>
//
// Columns and Grid overlap deliberately: Columns is the coarse two or three
// way split of a page, and its gaps are wider for that reason. Grid is the one
// to reach for when the items are cards and the count matters.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Columns {
props {
size: string = "default"
@@ -8,28 +18,92 @@ component Columns {
class: string = ""
}
functions {
shared function columnCount() {
var value = Number(columns)
if (!value || value < 1) {
return 1
}
return Math.min(4, value)
}
}
view {
<div
{...attrs}
data-ui-component="Columns"
class='wire-columns {class}'
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 items-start {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:md:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:gap-3='gap === "sm"'
class:gap-6='gap === "md"'
class:gap-10='gap === "lg"'
class:gap-14='gap === "xl"'
data-max-width='{maxWidth}'
data-columns='{columnCount()}'
>
<slot></slot>
</div>
}
style {
.wire-columns {
--columns-gap: 1.5rem;
display: grid;
width: 100%;
min-width: 0;
gap: var(--columns-gap);
grid-template-columns: 1fr;
align-items: start;
}
.wire-columns[data-max-width="md"] {
max-width: 48rem;
}
.wire-columns[data-max-width="lg"] {
max-width: 64rem;
}
.wire-columns[data-max-width="xl"] {
max-width: 80rem;
}
.wire-columns[data-max-width="2xl"] {
max-width: 96rem;
}
.wire-columns[data-max-width="full"] {
max-width: none;
}
.wire-columns[data-gap="sm"] {
--columns-gap: 0.75rem;
}
.wire-columns[data-gap="lg"] {
--columns-gap: 2.5rem;
}
.wire-columns[data-gap="xl"] {
--columns-gap: 3.5rem;
}
/*
* Splits at the tablet breakpoint rather than the phone one: a page split
* is worth keeping single-column for longer than a card deck is.
*/
@media (min-width: 768px) {
.wire-columns:not([data-columns="1"]) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.wire-columns[data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wire-columns[data-columns="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
}
}
+114 -9
View File
@@ -1,17 +1,122 @@
// CustomScrollbar -- a scrolling region with a themed scrollbar.
//
// <CustomScrollbar maxHeight="20rem">...long content...</CustomScrollbar>
//
// Scrollbar appearance is CSS, not script: scrollbar-width and scrollbar-color
// are the standard properties, and the ::-webkit-scrollbar rules cover the
// browsers that still need them.
//
// This component used to declare a scroll output it never emitted, and its
// props were columns, gap and maxWidth copied from a grid scaffold. The output
// is removed rather than left unimplemented -- a caller can listen for a plain
// scroll event on the element, which is what it would have been anyway.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component CustomScrollbar {
outputs {
scroll(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
size: string = "default"
axis: string = "vertical"
// Track thickness in pixels. Clamped, because it arrives as an attribute
// and a scrollbar wider than the content is not useful to anyone.
thickness: number = 8
maxHeight: string = "20rem"
radius: string = "999px"
class: string = ""
}
functions {
shared function trackSize() {
var value = Number(thickness)
if (!value || value < 2) {
return 8
}
return Math.min(24, value)
}
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--custom-scrollbar wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
{...attrs}
data-ui-component="CustomScrollbar"
class='wire-scrollbar {class}'
data-color='{color}'
data-size='{size}'
data-axis='{axis}'
style='--scrollbar-thickness:{trackSize()}px;--scrollbar-radius:{radius};max-height:{maxHeight};'
>
<slot />
</div>
}
style {
.wire-scrollbar {
--scrollbar-accent: var(--wire-color-primary);
min-width: 0;
overflow: auto;
/*
* The standard properties. Firefox and current Chrome honour these; the
* webkit rules below are only for the engines that still ignore them.
*/
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--scrollbar-accent) 55%, transparent) transparent;
overscroll-behavior: contain;
}
.wire-scrollbar[data-color="secondary"] {
--scrollbar-accent: var(--wire-color-secondary);
}
.wire-scrollbar[data-color="success"] {
--scrollbar-accent: var(--wire-color-success);
}
.wire-scrollbar[data-color="danger"] {
--scrollbar-accent: var(--wire-color-danger);
}
.wire-scrollbar[data-color="info"] {
--scrollbar-accent: var(--wire-color-info);
}
.wire-scrollbar[data-axis="vertical"] {
overflow-x: hidden;
overflow-y: auto;
}
.wire-scrollbar[data-axis="horizontal"] {
overflow-x: auto;
overflow-y: hidden;
}
.wire-scrollbar::-webkit-scrollbar {
width: var(--scrollbar-thickness, 8px);
height: var(--scrollbar-thickness, 8px);
}
.wire-scrollbar::-webkit-scrollbar-track {
background: var(--wire-color-surface-soft);
border-radius: var(--scrollbar-radius, 999px);
}
.wire-scrollbar::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--scrollbar-accent) 45%, transparent);
border-radius: var(--scrollbar-radius, 999px);
}
.wire-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-accent);
}
/*
* A pointer-less device paints its own overlay scrollbar and ignores the
* width above, so the region keeps its own padding instead.
*/
@media (hover: none) {
.wire-scrollbar {
scrollbar-width: auto;
}
}
}
}
+105 -25
View File
@@ -1,3 +1,13 @@
// Image -- a framed image with a reserved aspect ratio.
//
// <Image src="/hero.jpg" alt="Dashboard" size="video" rounded={true} />
//
// The frame keeps its ratio whether or not the image has loaded, so the page
// does not jump when it arrives. Without a src it renders a labelled
// placeholder of the same shape rather than collapsing.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Image {
props {
size: string = "default"
@@ -7,41 +17,111 @@ component Image {
width: string = ""
height: string = ""
loading: string = "lazy"
fit: string = "cover"
rounded: boolean = false
class: string = ""
}
view {
<figure
{...attrs}
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='rounded'
class='wire-image {class}'
data-size='{size}'
data-color='{color}'
data-fit='{fit}'
data-rounded='{rounded}'
>
{#if src}
<img
src='{src}'
alt='{alt}'
width='{width}'
height='{height}'
loading='{loading}'
decoding="async"
class="block h-auto w-full object-cover transition duration-300"
class:aspect-square='size === "square"'
class:aspect-video='size === "video"'
class:aspect-[4/3]='size === "landscape"'
class:aspect-[3/4]='size === "portrait"'
/>
{:else}
<div
class="flex min-h-48 w-full items-center justify-center text-[var(--wire-color-text-muted)]"
role="img"
aria-label='{alt || "Image placeholder"}'
>
<span class="icon-[lucide--image] size-8" aria-hidden="true"></span>
</div>
{/if}
<img
class="wire-image__img"
data-show="src"
src='{src}'
alt='{alt}'
width='{width}'
height='{height}'
loading='{loading}'
decoding="async"
/>
<div
class="wire-image__placeholder"
data-show="!src"
role="img"
aria-label='{alt || "Image placeholder"}'
>
<span class="icon-[lucide--image] wire-image__placeholder-icon" aria-hidden="true"></span>
</div>
<slot></slot>
</figure>
}
style {
.wire-image {
position: relative;
margin: 0;
overflow: hidden;
width: 100%;
min-width: 0;
background: var(--wire-color-surface-soft);
}
.wire-image[data-rounded="true"] {
border-radius: var(--wire-radius-lg);
}
/*
* The ratio sits on the frame rather than the image, so the space is
* reserved before the file arrives and the page does not jump.
*/
.wire-image[data-size="square"] {
aspect-ratio: 1 / 1;
}
.wire-image[data-size="video"] {
aspect-ratio: 16 / 9;
}
.wire-image[data-size="landscape"] {
aspect-ratio: 4 / 3;
}
.wire-image[data-size="portrait"] {
aspect-ratio: 3 / 4;
}
.wire-image__img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.wire-image[data-fit="contain"] .wire-image__img {
object-fit: contain;
}
.wire-image[data-fit="fill"] .wire-image__img {
object-fit: fill;
}
.wire-image[data-size="default"] .wire-image__img {
height: auto;
}
.wire-image__placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 12rem;
height: 100%;
color: var(--wire-color-text-muted);
}
.wire-image__placeholder-icon {
width: 2rem;
height: 2rem;
}
}
}
+16 -16
View File
@@ -232,26 +232,17 @@ component InputNumber {
)
}
// Outputs are resolved by name, so each one is written out. Raw
// CustomEvents dispatched on the root -- what this did before -- never
// reach a parent @binding.
client function dispatchInputNumberEvent(
sourceEvent,
eventName,
action,
previousValue,
root,
customEvent
payload
) {
root = sourceEvent.currentTarget.closest("[data-wrn-input-number]")
if (!root && sourceEvent.target) {
root = sourceEvent.target.closest("[data-wrn-input-number]")
}
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent(eventName, true, false, {
payload = {
component: "InputNumber",
name: name,
value: currentValue,
@@ -261,8 +252,17 @@ component InputNumber {
max: hasMax() ? Number(max) : null,
step: normalizedStep(),
valid: !isInvalid()
})
root.dispatchEvent(customEvent)
}
if (eventName === "input") {
output.input(payload)
} else if (eventName === "change") {
output.change(payload)
} else if (eventName === "increment") {
output.increment(payload)
} else if (eventName === "decrement") {
output.decrement(payload)
}
}
client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
+48 -2
View File
@@ -1,9 +1,55 @@
// Kbd -- a keyboard key.
//
// <Kbd label="Ctrl" />
//
// Deliberately small. A kbd element is a kbd element; the work here is making
// it look like a key and follow the theme.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Kbd {
props {
size: string = "default"
color: string = "primary"
label: string = "K"
label: string = "K"
class: string = ""
}
view { <kbd class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--kbd {class}"><slot>{label}</slot></kbd> }
view {
<kbd {...attrs} class='wire-kbd {class}' data-size='{size}' data-color='{color}'>
<slot>{label}</slot>
</kbd>
}
style {
.wire-kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.6rem;
padding: 0.15rem 0.4rem;
border: 1px solid var(--wire-color-border);
/* The lower edge is what reads as a physical key. */
border-bottom-width: 2px;
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface-soft);
color: var(--wire-color-text);
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
line-height: 1;
}
.wire-kbd[data-size="sm"] {
min-width: 1.4rem;
padding: 0.1rem 0.3rem;
font-size: 0.7rem;
}
.wire-kbd[data-size="lg"] {
min-width: 2rem;
padding: 0.3rem 0.55rem;
font-size: 0.9rem;
}
}
}
+214 -8
View File
@@ -1,19 +1,225 @@
// LayoutSplitter -- two panes with a divider the reader can move.
//
// <LayoutSplitter size={40} minSize={20}>
// <div data-slot="start">...</div>
// <div data-slot="end">...</div>
// </LayoutSplitter>
//
// The dragging lives in the reactive runtime behind data-wrn-splitter. Pointer
// moves fire far too often to route through a client function, and a state
// write made inside a pointermove callback is dropped, so the resolved size is
// held in the DOM as the --wrn-split custom property and these styles read it.
//
// This component previously declared resizeStart, resize and resizeEnd with no
// pointer handling whatsoever: a caller wired up @resize and received nothing,
// for ever, with no error.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component LayoutSplitter {
outputs {
resizeStart(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
resize(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
resizeEnd(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
// Named sizeChange rather than resize so it cannot be confused with the
// native window event a caller may already be listening for. An earlier
// comment here claimed a natively-named output could never reach a parent
// binding; that was wrong, and the rename was never what fixed anything.
sizeChange(payload: { size: number })
}
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
size: number = 50
orientation: string = "horizontal"
// Smallest share either pane may take, as a percentage. It also fixes the
// upper bound at 100 - minSize, so neither pane can be dragged away to
// nothing and left unrecoverable by pointer.
minSize: number = 15
step: number = 5
label: string = "Resize panels"
class: string = ""
}
functions {
// The runtime resolves the size and announces it; this turns that into the
// declared output so a parent @resize binding receives it.
client function reportResize(sourceEvent) {
var detail = sourceEvent ? sourceEvent.detail : null
if (!detail) {
return
}
output.sizeChange({ size: detail.size })
}
shared function isVertical() {
return orientation === "vertical"
}
shared function lowerBound() {
var value = Number(minSize)
if (!value || value < 0) {
return 15
}
return Math.min(45, value)
}
shared function upperBound() {
return 100 - lowerBound()
}
// Sizes arrive as HTML attributes, so a value outside the bounds is
// routine rather than exceptional. Clamp instead of rendering something
// the reader cannot undo.
shared function currentSize() {
var value = Number(size)
if (!value || value < 0) {
return 50
}
return Math.min(upperBound(), Math.max(lowerBound(), value))
}
shared function stepSize() {
var value = Number(step)
return value && value > 0 ? value : 5
}
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--layout-splitter wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
{...attrs}
data-ui-component="LayoutSplitter"
class='wire-splitter {class}'
data-color='{color}'
data-orientation='{orientation}'
data-wrn-splitter='{isVertical() ? "vertical" : "horizontal"}'
data-wrn-splitter-min='{lowerBound()}'
data-wrn-splitter-step='{stepSize()}'
style='--wrn-split:{currentSize()}%;'
@wrnexus:splitter:resize='reportResize(event)'
>
<div class="wire-splitter__pane wire-splitter__pane--start">
<slot name="start"></slot>
</div>
<div
class="wire-splitter__handle"
data-wrn-splitter-handle="true"
role="separator"
tabindex="0"
aria-label='{label}'
aria-orientation='{isVertical() ? "horizontal" : "vertical"}'
aria-valuenow='{currentSize()}'
aria-valuemin='{lowerBound()}'
aria-valuemax='{upperBound()}'
>
<span class="wire-splitter__grip" aria-hidden="true"></span>
</div>
<div class="wire-splitter__pane wire-splitter__pane--end">
<slot name="end"></slot>
</div>
<slot />
</div>
}
style {
.wire-splitter {
--splitter-accent: var(--wire-color-primary);
display: grid;
/* The first track follows the size the runtime resolves. */
grid-template-columns: var(--wrn-split, 50%) auto minmax(0, 1fr);
align-items: stretch;
width: 100%;
min-width: 0;
}
.wire-splitter[data-color="secondary"] {
--splitter-accent: var(--wire-color-secondary);
}
.wire-splitter[data-color="success"] {
--splitter-accent: var(--wire-color-success);
}
.wire-splitter[data-color="danger"] {
--splitter-accent: var(--wire-color-danger);
}
.wire-splitter[data-color="info"] {
--splitter-accent: var(--wire-color-info);
}
.wire-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--wrn-split, 50%) auto minmax(0, 1fr);
}
.wire-splitter__pane {
min-width: 0;
min-height: 0;
overflow: auto;
}
.wire-splitter__handle {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0 0.25rem;
border: 0;
background: transparent;
cursor: col-resize;
/* Without this the pointer drag selects the text in both panes. */
touch-action: none;
user-select: none;
}
.wire-splitter[data-orientation="vertical"] .wire-splitter__handle {
padding: 0.25rem 0;
cursor: row-resize;
}
.wire-splitter__grip {
display: block;
width: 2px;
height: 100%;
min-height: 1.5rem;
border-radius: 999px;
background: var(--wire-color-border);
transition: background 140ms ease;
}
.wire-splitter[data-orientation="vertical"] .wire-splitter__grip {
width: 100%;
min-width: 1.5rem;
height: 2px;
}
.wire-splitter__handle:hover .wire-splitter__grip,
.wire-splitter[data-wrn-splitter-dragging="true"] .wire-splitter__grip {
background: var(--splitter-accent);
}
.wire-splitter__handle:focus-visible {
outline: 2px solid var(--splitter-accent);
outline-offset: -2px;
border-radius: var(--wire-radius-sm);
}
/*
* Two panes side by side stop making sense on a phone. Stacking them keeps
* both readable, and the divider stops being draggable because the grid
* no longer has a second track to trade against.
*/
@media (max-width: 639px) {
.wire-splitter,
.wire-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
}
.wire-splitter__handle {
cursor: default;
}
}
}
}
+114 -24
View File
@@ -1,3 +1,13 @@
// Link -- a text link that follows the theme.
//
// <Link href="/docs" label="Documentation" />
// <Link href="https://example.com" label="Spec" external={true} />
//
// external adds rel="noopener noreferrer" and an outbound icon, so the reader
// is told the link leaves the site before they follow it.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Link {
props {
size: string = "default"
@@ -6,42 +16,122 @@ component Link {
href: string = "#"
target: string = ""
rel: string = ""
underline: string = "hover"
external: boolean = false
class: string = ""
}
view {
<a
{...attrs}
data-ui-component="Link"
class='wire-link {class}'
href='{href}'
target='{target}'
rel='{external ? (rel || "noopener noreferrer") : rel}'
class='inline-flex min-w-0 items-center gap-1.5 rounded-md font-semibold underline-offset-4 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-background)] {class}'
class:text-xs='size === "xs"'
class:text-sm='size === "sm" || size === "default"'
class:text-base='size === "md"'
class:text-lg='size === "lg"'
class:text-[var(--wire-color-primary)]='color === "primary"'
class:hover:text-[var(--wire-color-primary-hover)]='color === "primary"'
class:text-[var(--wire-color-secondary)]='color === "secondary"'
class:hover:text-[var(--wire-color-secondary-hover)]='color === "secondary"'
class:text-[var(--wire-color-success)]='color === "success"'
class:text-[var(--wire-color-warning-text)]='color === "warning"'
class:text-[var(--wire-color-danger)]='color === "danger"'
class:text-[var(--wire-color-info)]='color === "info"'
class:text-[var(--wire-color-text)]='color === "neutral"'
class:hover:underline='external === false'
data-size='{size}'
data-color='{color}'
data-underline='{underline}'
data-external='{external}'
>
<span class="truncate">{label}</span>
{#if external}
<span
class="icon-[lucide--external-link] size-3.5 shrink-0"
aria-hidden="true"
></span>
{/if}
<span class="wire-link__label">{label}</span>
<span
class="icon-[lucide--external-link] wire-link__icon"
data-show="external"
aria-hidden="true"
></span>
<slot></slot>
</a>
}
style {
.wire-link {
--link-tone: var(--wire-color-primary);
--link-tone-hover: var(--wire-color-primary-hover);
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-width: 0;
border-radius: var(--wire-radius-sm);
color: var(--link-tone);
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
text-underline-offset: 4px;
transition: color 140ms ease;
}
.wire-link[data-color="secondary"] {
--link-tone: var(--wire-color-secondary);
--link-tone-hover: var(--wire-color-secondary-hover);
}
.wire-link[data-color="success"] {
--link-tone: var(--wire-color-success);
--link-tone-hover: var(--wire-color-success);
}
.wire-link[data-color="warning"] {
--link-tone: var(--wire-color-warning-text);
--link-tone-hover: var(--wire-color-warning-text);
}
.wire-link[data-color="danger"] {
--link-tone: var(--wire-color-danger);
--link-tone-hover: var(--wire-color-danger);
}
.wire-link[data-color="info"] {
--link-tone: var(--wire-color-info);
--link-tone-hover: var(--wire-color-info);
}
.wire-link[data-color="neutral"] {
--link-tone: var(--wire-color-text);
--link-tone-hover: var(--wire-color-text);
}
.wire-link[data-size="xs"] {
font-size: 0.75rem;
}
.wire-link[data-size="md"] {
font-size: 1rem;
}
.wire-link[data-size="lg"] {
font-size: 1.125rem;
}
.wire-link[data-underline="always"] {
text-decoration: underline;
}
.wire-link[data-underline="hover"]:hover {
text-decoration: underline;
}
.wire-link:hover {
color: var(--link-tone-hover);
}
.wire-link:focus-visible {
outline: 2px solid var(--wire-color-focus);
outline-offset: 2px;
}
/* The label truncates; the outbound icon must not shrink with it. */
.wire-link__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-link__icon {
flex: 0 0 auto;
width: 0.875rem;
height: 0.875rem;
}
}
}
+5 -1
View File
@@ -1,4 +1,8 @@
component List {
outputs {
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -52,7 +56,7 @@ component List {
class:py-2.5='size === "sm"'
class:px-5='size === "lg"'
class:py-4='size === "lg"'
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
@click='output.select({ item: item, index: index })'
>
{#if item.icon}
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
+11 -3
View File
@@ -1,4 +1,12 @@
component Map {
outputs {
// markerClick and select both fire for a marker press; select is the
// generic name callers reach for, markerClick the explicit one.
markerClick(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
zoom(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -54,7 +62,7 @@ component Map {
aria-label='{item.label || item.title || "Map marker"}'
class="absolute inline-flex size-10 items-center justify-center rounded-full border-4 border-[var(--wire-color-surface-raised)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-lg transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
@click='event.currentTarget.dispatchEvent(new CustomEvent("markerClick", { bubbles: true, detail: item })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: item }))'
@click='output.markerClick(item); output.select(item)'
>
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
</button>
@@ -66,7 +74,7 @@ component Map {
type="button"
aria-label="Zoom in"
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "in" } }))'
@click='output.zoom({ direction: "in" })'
>
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
</button>
@@ -74,7 +82,7 @@ component Map {
type="button"
aria-label="Zoom out"
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "out" } }))'
@click='output.zoom({ direction: "out" })'
>
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
</button>
+9 -3
View File
@@ -1,4 +1,10 @@
component Marquee {
outputs {
// Fired when the reader pauses the scroll, by hover, focus or the button.
pause(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
resume(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -27,8 +33,8 @@ component Marquee {
<div
class="group relative min-w-0 flex-1 overflow-hidden"
@mouseenter='paused = true; event.currentTarget.dispatchEvent(new CustomEvent("pause", { bubbles: true }))'
@mouseleave='paused = false; event.currentTarget.dispatchEvent(new CustomEvent("resume", { bubbles: true }))'
@mouseenter='paused = true; output.pause({})'
@mouseleave='paused = false; output.resume({})'
@focusin='paused = true'
@focusout='paused = false'
>
@@ -77,7 +83,7 @@ component Marquee {
type="button"
aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
class="flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] px-4 text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
@click='paused = !paused; event.currentTarget.dispatchEvent(new CustomEvent(paused ? "pause" : "resume", { bubbles: true }))'
@click='paused = !paused; paused ? output.pause({}) : output.resume({})'
>
<span class="icon-[lucide--pause] size-4" data-show='!paused' aria-hidden="true"></span>
<span class="icon-[lucide--play] size-4" data-show='paused' aria-hidden="true"></span>
+333 -12
View File
@@ -1,23 +1,344 @@
// MegaMenu -- a trigger and a wide panel of grouped links.
//
// <MegaMenu label="Products" columns='[{"heading":"Platform",
// "items":[{"label":"Runtime","href":"/runtime"}]}]' />
//
// Deliberately one level deep. A mega menu exists to show breadth flat, so
// everything is one click away; nesting inside the panel buries content behind
// hover-within-hover and is close to unusable by keyboard and touch. Use Nav
// when you actually want cascading submenus.
//
// The panel carries data-wrn-anchored so the runtime clamp keeps it inside the
// viewport instead of hanging off the edge of a wide layout.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component MegaMenu {
outputs {
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
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)
open(payload: { sourceEvent: Event })
close(payload: { reason: string })
select(payload: { item: object; value: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Mega Menu"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
size: string = "default"
label: string = "Menu"
icon: string = ""
columns: unknown[] = []
footer: string = ""
defaultOpen: boolean = false
class: string = ""
}
state visible = defaultOpen
functions {
shared function columnList() {
return Array.isArray(columns) ? columns : []
}
shared function itemsOf(column) {
return column && Array.isArray(column.items) ? column.items : []
}
client function showPanel(sourceEvent) {
visible = true
output.open({ sourceEvent: sourceEvent })
}
client function hidePanel(reason) {
visible = false
output.close({ reason: reason })
}
client function togglePanel(sourceEvent) {
if (visible) {
hidePanel("toggle")
} else {
showPanel(sourceEvent)
}
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape" && visible) {
sourceEvent.preventDefault()
hidePanel("escape")
}
}
client function choose(item) {
if (item.disabled) {
return
}
output.select({ item: item, value: item.value || item.label || "" })
hidePanel("select")
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--mega-menu wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
<div
{...attrs}
data-ui-component="MegaMenu"
class='wire-mega {class}'
data-color='{color}'
data-size='{size}'
data-open='{visible}'
@keydown='handleKeydown(event)'
@mouseleave='hidePanel("pointer-leave")'
>
<button
type="button"
class="wire-mega__trigger"
aria-haspopup="true"
aria-expanded='{visible}'
@click='togglePanel(event)'
@mouseenter='showPanel(event)'
@focus='showPanel(event)'
>
<span class='wire-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
<span class="wire-mega__trigger-label">{label}</span>
<span class="wire-mega__arrow" aria-hidden="true">&#8250;</span>
</button>
<div
class="wire-mega__panel"
data-wrn-anchored="true"
data-show="visible"
data-wrn-roving="both"
>
<div class="wire-mega__columns">
{#each columnList() as column}
<section class="wire-mega__column">
<h3 class="wire-mega__heading" data-show="column.heading">{column.heading}</h3>
<ul class="wire-mega__list">
{#each itemsOf(column) as item}
<li>
<a
class="wire-mega__link"
href='{item.href || "#"}'
data-wrn-roving-item="true"
aria-disabled='{item.disabled ? "true" : "false"}'
@click='choose(item)'
>
<span
class='wire-mega__link-icon {item.icon}'
data-show="item.icon"
aria-hidden="true"
></span>
<span class="wire-mega__link-body">
<span class="wire-mega__link-label">{item.label}</span>
<span class="wire-mega__link-description" data-show="item.description">
{item.description}
</span>
</span>
</a>
</li>
{/each}
</ul>
</section>
{/each}
</div>
<p class="wire-mega__footer" data-show="footer">{footer}</p>
<slot />
</div>
</div>
}
style {
.wire-mega {
--mega-accent: var(--wire-color-primary);
position: relative;
display: inline-block;
max-width: 100%;
}
.wire-mega[data-color="secondary"] {
--mega-accent: var(--wire-color-secondary);
}
.wire-mega[data-color="success"] {
--mega-accent: var(--wire-color-success);
}
.wire-mega[data-color="danger"] {
--mega-accent: var(--wire-color-danger);
}
.wire-mega[data-color="info"] {
--mega-accent: var(--wire-color-info);
}
.wire-mega[data-size="sm"] {
font-size: 0.82rem;
}
.wire-mega[data-size="lg"] {
font-size: 1rem;
}
.wire-mega__trigger {
appearance: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.7rem;
border: 1px solid transparent;
border-radius: var(--wire-radius-sm);
background: transparent;
color: var(--wire-color-text-muted);
font: inherit;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
}
.wire-mega__trigger:hover,
.wire-mega[data-open="true"] .wire-mega__trigger {
background: var(--wire-color-surface-soft);
color: var(--wire-color-text);
}
.wire-mega__trigger:focus-visible {
outline: 2px solid var(--mega-accent);
outline-offset: 2px;
}
.wire-mega__arrow {
display: inline-block;
transition: transform 160ms ease;
}
.wire-mega[data-open="true"] .wire-mega__arrow {
transform: rotate(90deg);
}
/*
* The panel is offset below the trigger for looks, which used to close the
* menu on the way to it: that offset is dead space belonging to neither
* element, so crossing it fired mouseleave on the root. The bridge below
* covers the gap with a descendant, so the pointer never actually leaves.
*/
.wire-mega__panel::before {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 100%;
height: 0.6rem;
}
.wire-mega__panel {
position: absolute;
z-index: 40;
top: calc(100% + 0.4rem);
left: 0;
width: max-content;
max-width: min(60rem, calc(100vw - 2rem));
padding: 1rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-lg);
background: var(--wire-color-surface);
box-shadow: var(--wire-shadow-1);
}
.wire-mega__columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 1.25rem;
}
.wire-mega__column {
min-width: 0;
}
.wire-mega__heading {
margin: 0 0 0.5rem;
color: var(--wire-color-text-muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.wire-mega__list {
display: grid;
gap: 0.15rem;
margin: 0;
padding: 0;
list-style: none;
}
.wire-mega__link {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.45rem 0.5rem;
border-radius: var(--wire-radius-sm);
color: var(--wire-color-text);
text-decoration: none;
}
.wire-mega__link:hover {
background: var(--wire-color-surface-soft);
}
.wire-mega__link:focus-visible {
outline: 2px solid var(--mega-accent);
outline-offset: 2px;
}
.wire-mega__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wire-mega__link-body {
display: flex;
flex-direction: column;
min-width: 0;
}
.wire-mega__link-label {
font-size: 0.88rem;
font-weight: 600;
}
.wire-mega__link-description {
color: var(--wire-color-text-muted);
font-size: 0.78rem;
line-height: 1.5;
}
.wire-mega__footer {
margin: 1rem 0 0;
padding-top: 0.75rem;
border-top: 1px solid var(--wire-color-border);
color: var(--wire-color-text-muted);
font-size: 0.8rem;
}
/*
* On a phone the panel stops floating and becomes part of the flow. A
* pointer-opened overlay pinned to a trigger is unreachable on touch, and
* a 60rem grid has nowhere to go on a 360px screen.
*/
@media (max-width: 767px) {
.wire-mega {
display: block;
}
.wire-mega__panel {
position: static;
width: auto;
max-width: 100%;
box-shadow: none;
}
.wire-mega__columns {
grid-template-columns: 1fr;
}
}
}
}
+338 -6
View File
@@ -1,22 +1,354 @@
// Nav -- a navigation link list, flat or with submenus.
//
// <Nav items='[{"label":"Home","href":"/","value":"home"}]' active="home" />
//
// An item carrying its own items array becomes a submenu. Depth is capped at
// three levels: this template language has no component recursion, so each
// level is written out, and three covers any realistic navigation.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Nav {
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)
change(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)
select(payload: { item: object; value: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Nav"
size: string = "default"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
label: string = "Main"
collapsible: boolean = true
toggleLabel: string = "Menu"
class: string = ""
}
state expanded = false
functions {
shared function itemList() {
return Array.isArray(items) ? items : []
}
shared function childrenOf(item) {
return item && Array.isArray(item.items) ? item.items : []
}
shared function isActive(item) {
return Boolean(item.value) && item.value === active
}
shared function rovingAxis() {
return orientation === "vertical" ? "vertical" : "horizontal"
}
client function choose(item) {
if (item.disabled) {
return
}
output.select({ item: item, value: item.value || "" })
}
client function toggleMenu() {
expanded = !expanded
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--nav wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<nav
{...attrs}
data-ui-component="Nav"
class='wire-nav {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-expanded='{expanded}'
role="navigation"
aria-label='{label}'
>
<button
type="button"
class="wire-nav__toggle"
data-show="collapsible"
aria-expanded='{expanded}'
aria-label='{toggleLabel}'
@click='toggleMenu()'
>
<span class="wire-nav__toggle-bar" aria-hidden="true"></span>
<span class="wire-nav__toggle-text">{toggleLabel}</span>
</button>
<ul class="wire-nav__list" data-wrn-roving='{rovingAxis()}'>
{#each itemList() as item}
<li class="wire-nav__item" data-has-children='{childrenOf(item).length > 0}'>
<a
class="wire-nav__link"
href='{item.href || "#"}'
data-wrn-roving-item="true"
data-active='{isActive(item)}'
aria-current='{isActive(item) ? "page" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
@click='choose(item)'
>
<span
class='wire-nav__icon {item.icon}'
data-show="item.icon"
aria-hidden="true"
></span>
<span class="wire-nav__label">{item.label}</span>
<span class="wire-nav__badge" data-show="item.badge">{item.badge}</span>
<span
class="wire-nav__arrow"
data-show="childrenOf(item).length > 0"
aria-hidden="true"
>&#8250;</span>
</a>
<ul
class="wire-nav__submenu"
data-wrn-anchored="true"
data-show="childrenOf(item).length > 0"
>
{#each childrenOf(item) as child}
<li class="wire-nav__item" data-has-children='{childrenOf(child).length > 0}'>
<a
class="wire-nav__link"
href='{child.href || "#"}'
data-active='{isActive(child)}'
aria-current='{isActive(child) ? "page" : "false"}'
aria-disabled='{child.disabled ? "true" : "false"}'
@click='choose(child)'
>
<span
class='wire-nav__icon {child.icon}'
data-show="child.icon"
aria-hidden="true"
></span>
<span class="wire-nav__label">{child.label}</span>
<span class="wire-nav__badge" data-show="child.badge">{child.badge}</span>
<span
class="wire-nav__arrow"
data-show="childrenOf(child).length > 0"
aria-hidden="true"
>&#8250;</span>
</a>
<ul
class="wire-nav__submenu wire-nav__submenu--level3"
data-wrn-anchored="true"
data-show="childrenOf(child).length > 0"
>
{#each childrenOf(child) as leaf}
<li class="wire-nav__item">
<a
class="wire-nav__link"
href='{leaf.href || "#"}'
data-active='{isActive(leaf)}'
aria-current='{isActive(leaf) ? "page" : "false"}'
aria-disabled='{leaf.disabled ? "true" : "false"}'
@click='choose(leaf)'
>
<span
class='wire-nav__icon {leaf.icon}'
data-show="leaf.icon"
aria-hidden="true"
></span>
<span class="wire-nav__label">{leaf.label}</span>
</a>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
<slot />
</nav>
}
style {
.wire-nav {
--nav-accent: var(--wire-color-primary);
max-width: 100%;
}
.wire-nav[data-color="secondary"] {
--nav-accent: var(--wire-color-secondary);
}
.wire-nav[data-color="success"] {
--nav-accent: var(--wire-color-success);
}
.wire-nav[data-color="danger"] {
--nav-accent: var(--wire-color-danger);
}
.wire-nav[data-color="info"] {
--nav-accent: var(--wire-color-info);
}
.wire-nav[data-size="sm"] {
font-size: 0.82rem;
}
.wire-nav[data-size="lg"] {
font-size: 1rem;
}
.wire-nav__list {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.wire-nav[data-orientation="vertical"] .wire-nav__list {
flex-direction: column;
align-items: stretch;
}
.wire-nav__item {
position: relative;
}
.wire-nav__link {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.45rem 0.7rem;
border-radius: var(--wire-radius-sm);
color: var(--wire-color-text-muted);
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
}
.wire-nav__link:hover {
background: var(--wire-color-surface-soft);
color: var(--wire-color-text);
}
.wire-nav__link:focus-visible {
outline: 2px solid var(--nav-accent);
outline-offset: 2px;
}
.wire-nav__link[data-active="true"] {
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
}
.wire-nav__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wire-nav__badge {
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
font-size: 0.72rem;
}
.wire-nav__arrow {
display: inline-block;
transition: transform 160ms ease;
}
.wire-nav__item:hover > .wire-nav__link > .wire-nav__arrow,
.wire-nav__item:focus-within > .wire-nav__link > .wire-nav__arrow {
transform: rotate(90deg);
}
.wire-nav__submenu {
position: absolute;
z-index: 30;
top: 100%;
left: 0;
min-width: 12rem;
margin: 0;
padding: 0.35rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface);
box-shadow: var(--wire-shadow-1);
list-style: none;
opacity: 0;
visibility: hidden;
transition: opacity 160ms ease;
}
.wire-nav__item:hover > .wire-nav__submenu,
.wire-nav__item:focus-within > .wire-nav__submenu {
opacity: 1;
visibility: visible;
}
.wire-nav__submenu--level3 {
top: 0;
left: 100%;
}
.wire-nav__toggle {
display: none;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface);
color: var(--wire-color-text);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.wire-nav__toggle-bar {
width: 1rem;
height: 2px;
background: currentColor;
box-shadow:
0 -5px 0 currentColor,
0 5px 0 currentColor;
}
/*
* On a phone the bar becomes a disclosure: submenus stop being floating
* overlays and stack inline, because a hover-opened overlay is
* unreachable on touch.
*/
@media (max-width: 767px) {
.wire-nav__toggle {
display: inline-flex;
}
.wire-nav__list {
flex-direction: column;
align-items: stretch;
}
.wire-nav[data-expanded="false"] .wire-nav__list {
display: none;
}
.wire-nav__submenu,
.wire-nav__submenu--level3 {
position: static;
opacity: 1;
visibility: visible;
border: 0;
box-shadow: none;
padding-left: 1rem;
}
}
}
}
+441 -5
View File
@@ -86,11 +86,11 @@ size: string = "default"
</button>
<div class="wire-navbar__collapse {mobileOpen ? 'is-open' : ''}">
<nav class="wire-navbar__menus" aria-label="{label}">
<nav class="wire-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
{#each items as item}
{#if item.children && item.children.length}
<details class="wire-navbar__dropdown wire-navbar__dropdown--{item.type || 'dropdown'}" name="wire-navbar-menu" @toggle="toggleDropdown(event, item)">
<summary aria-current="{isItemActive(item) ? 'page' : ''}">
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
<span class="wire-navbar__chevron" aria-hidden="true"></span>
@@ -103,13 +103,13 @@ size: string = "default"
{#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
{#if child.description}<small>{child.description}</small>{/if}
{#each child.children as nested}
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : ''}" @click="selectItem(nested, 3)">
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
<span><strong>{nested.label}</strong>{#if nested.description}<small>{nested.description}</small>{/if}</span>
</a>
{/each}
{:else}
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2)">
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
<span><strong>{child.label}</strong>{#if child.description}<small>{child.description}</small>{/if}</span>
</a>
@@ -119,7 +119,7 @@ size: string = "default"
</div>
</details>
{:else}
<a class="wire-navbar__menu-link" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
<a class="wire-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
</a>
@@ -140,4 +140,440 @@ size: string = "default"
</div>
</header>
}
style {
/* Full application navigation */
.wire-navbar {
position: relative;
z-index: 40;
width: 100%;
color: var(--wire-color-text);
background: color-mix(in srgb, var(--wire-color-surface) 96%, transparent);
border-bottom: 1px solid var(--wire-color-border);
}
.wire-navbar--sticky {
position: sticky;
top: 0;
backdrop-filter: blur(16px);
}
.wire-navbar__topbar:empty,
.wire-navbar__topbar:not(:has(*)) {
display: none;
}
.wire-navbar__topbar:not(:empty) {
display: flex;
min-height: 2.25rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.35rem clamp(1rem, 4vw, 3rem);
color: var(--wire-color-muted);
background: var(--wire-color-surface-2);
border-bottom: 1px solid var(--wire-color-border);
font-size: 0.75rem;
font-weight: 400;
}
.wire-navbar__main {
display: flex;
min-height: 4.25rem;
width: min(100%, 90rem);
margin-inline: auto;
padding: 0.75rem clamp(1rem, 4vw, 3rem);
align-items: center;
gap: 1.5rem;
}
.wire-navbar--width-full .wire-navbar__main {
width: 100%;
max-width: none;
}
.wire-navbar--width-compact .wire-navbar__main {
width: min(86%, 80rem);
}
.wire-navbar__brand,
.wire-navbar__menu-link,
.wire-navbar__dropdown summary,
.wire-navbar__action,
.wire-navbar__panel a {
color: inherit;
text-decoration: none;
}
.wire-navbar__brand {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 0.75rem;
flex: 0 0 auto;
}
.wire-navbar__brand-logo {
display: block;
width: auto;
max-width: 11rem;
height: 2.5rem;
object-fit: contain;
}
.wire-navbar__brand-icon {
width: 2.5rem;
height: 2.5rem;
color: var(--wire-component-color, var(--wire-color-primary));
}
.wire-navbar__brand-copy {
display: grid;
min-width: 0;
line-height: 1.2;
}
.wire-navbar__brand-copy strong {
font-size: 0.9rem;
font-weight: 600;
}
.wire-navbar__brand-copy small {
margin-top: 0.2rem;
color: var(--wire-color-muted);
font-size: 0.6875rem;
font-weight: 400;
}
.wire-navbar__collapse,
.wire-navbar__menus,
.wire-navbar__actions {
display: flex;
align-items: center;
}
.wire-navbar__collapse {
min-width: 0;
flex: 1;
gap: 1rem;
}
.wire-navbar__menus {
justify-content: center;
flex: 1;
gap: 0.25rem;
}
.wire-navbar__actions {
justify-content: flex-end;
gap: 0.5rem;
}
.wire-navbar__menu-link,
.wire-navbar__dropdown > summary,
.wire-navbar__action {
display: inline-flex;
min-height: 2.75rem;
padding: 0.65rem 0.8rem;
align-items: center;
gap: 0.45rem;
border-radius: 0.65rem;
cursor: pointer;
font-size: 0.8125rem;
font-weight: 500;
white-space: nowrap;
}
.wire-navbar__menu-link:hover,
.wire-navbar__dropdown > summary:hover {
color: var(--wire-color-text);
background: var(--wire-color-surface-2);
}
.wire-navbar__menu-link[aria-current="page"],
.wire-navbar__dropdown > summary[aria-current="page"] {
color: var(--wire-component-color, var(--wire-color-primary));
font-weight: 600;
background: color-mix(
in srgb,
var(--wire-component-color, var(--wire-color-primary)) 12%,
transparent
);
box-shadow: inset 0 -2px 0 var(--wire-component-color, var(--wire-color-primary));
}
.wire-navbar__menu-link[aria-current="page"]:hover,
.wire-navbar__dropdown > summary[aria-current="page"]:hover {
color: var(--wire-component-color, var(--wire-color-primary));
background: color-mix(
in srgb,
var(--wire-component-color, var(--wire-color-primary)) 16%,
transparent
);
}
.wire-navbar__dropdown {
position: relative;
}
.wire-navbar__dropdown > summary {
list-style: none;
}
.wire-navbar__dropdown > summary::-webkit-details-marker {
display: none;
}
.wire-navbar__chevron {
width: 0.45rem;
height: 0.45rem;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(45deg) translateY(-0.15rem);
transition: transform var(--wire-motion-fast, 150ms) ease;
}
.wire-navbar__dropdown[open] .wire-navbar__chevron {
transform: rotate(225deg) translate(-0.1rem, -0.1rem);
}
.wire-navbar__panel {
position: absolute;
top: calc(100% + 0.55rem);
left: 50%;
display: grid;
width: max-content;
min-width: 15rem;
max-width: min(90vw, 64rem);
padding: 0.65rem;
gap: 0.45rem;
border: 1px solid var(--wire-color-border);
border-radius: 0.9rem;
color: var(--wire-color-text);
background: var(--wire-color-surface);
box-shadow: 0 18px 48px rgb(15 23 42 / 0.16);
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
transform-origin: top center;
transition:
opacity 180ms ease,
transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
display 180ms allow-discrete;
}
.wire-navbar__panel::before {
position: absolute;
right: 0;
bottom: 100%;
left: 0;
height: 0.65rem;
content: "";
}
.wire-navbar__dropdown:not([open]) > .wire-navbar__panel {
display: none;
opacity: 0;
transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
}
@starting-style {
.wire-navbar__dropdown[open] > .wire-navbar__panel {
opacity: 0;
transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
}
}
.wire-navbar__dropdown--mega {
position: static;
}
.wire-navbar__dropdown--mega .wire-navbar__panel {
right: auto;
left: 50%;
width: min(calc(100vw - 2rem), 64rem);
}
.wire-navbar__panel--columns-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wire-navbar__panel--columns-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wire-navbar__panel--columns-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.wire-navbar__panel-intro {
grid-column: 1 / -1;
margin: 0;
padding: 0.5rem 0.65rem;
color: var(--wire-color-muted);
font-size: 0.8125rem;
font-weight: 400;
}
.wire-navbar__group {
display: grid;
align-content: start;
gap: 0.25rem;
}
.wire-navbar__group-title {
padding: 0.55rem 0.65rem 0.25rem;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.wire-navbar__group > small {
padding: 0 0.65rem 0.4rem;
color: var(--wire-color-muted);
font-size: 0.7rem;
font-weight: 400;
}
.wire-navbar__panel a {
display: flex;
min-height: 2.75rem;
padding: 0.65rem;
align-items: flex-start;
gap: 0.65rem;
border-radius: 0.6rem;
font-size: 0.8rem;
font-weight: 450;
}
.wire-navbar__panel a:hover {
background: var(--wire-color-surface-2);
}
.wire-navbar__panel a span:last-child {
display: grid;
gap: 0.15rem;
}
.wire-navbar__panel a strong {
font-weight: 500;
}
.wire-navbar__panel a small {
color: var(--wire-color-muted);
font-size: 0.7rem;
font-weight: 400;
}
.wire-navbar__action {
border: 1px solid transparent;
}
.wire-navbar__action--primary {
color: var(--wire-color-on-primary, #fff);
background: var(--wire-component-color, var(--wire-color-primary));
font-weight: 600;
}
.wire-navbar__action--outline {
border-color: var(--wire-color-border);
}
.wire-navbar__toggle {
display: none;
width: 2.75rem;
height: 2.75rem;
margin-left: auto;
padding: 0.65rem;
border: 1px solid var(--wire-color-border);
border-radius: 0.65rem;
color: inherit;
background: var(--wire-color-surface);
}
.wire-navbar__toggle span {
display: block;
height: 2px;
margin: 0.25rem 0;
background: currentColor;
}
@media (max-width: 900px) {
.wire-navbar--width-compact .wire-navbar__main {
width: 100%;
}
.wire-navbar__toggle {
display: block;
}
.wire-navbar__collapse {
position: absolute;
top: 100%;
right: 0;
left: 0;
display: none;
padding: 0.75rem 1rem 1rem;
align-items: stretch;
flex-direction: column;
background: var(--wire-color-surface);
border-bottom: 1px solid var(--wire-color-border);
box-shadow: 0 16px 32px rgb(15 23 42 / 0.12);
max-height: calc(100dvh - 4rem);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.wire-navbar__collapse.is-open {
display: flex;
}
.wire-navbar__menus,
.wire-navbar__actions {
width: 100%;
align-items: stretch;
flex-direction: column;
}
.wire-navbar__menu-link,
.wire-navbar__dropdown > summary,
.wire-navbar__action {
width: 100%;
}
.wire-navbar__panel,
.wire-navbar__dropdown--mega .wire-navbar__panel {
position: static;
width: 100%;
max-width: none;
margin-top: 0.25rem;
box-shadow: none;
transform: none;
overflow: hidden;
animation: wire-navbar-mobile-panel 180ms ease both;
}
.wire-navbar__dropdown:not([open]) > .wire-navbar__panel,
.wire-navbar__dropdown[open] > .wire-navbar__panel {
transform: none;
}
.wire-navbar__panel[class*="wire-navbar__panel--columns-"] {
grid-template-columns: 1fr;
}
.wire-navbar__panel::before {
display: none;
}
}
@media (max-width: 600px) {
.wire-navbar__brand-copy small {
display: none;
}
}
@keyframes wire-navbar-mobile-panel {
from {
opacity: 0;
transform: translateY(-0.35rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
}
+277 -9
View File
@@ -1,23 +1,291 @@
// Pagination -- page controls over a known total.
//
// <Pagination page={2} pageSize={10} total={137} variant="numbered" />
//
// The component owns no data. It reports the requested page through its
// change output and lets the caller fetch or slice.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Pagination {
outputs {
change(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)
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { page: number; pageSize: number })
previous(payload: { page: number })
next(payload: { page: number })
}
props {
size: string = "default"
color: string = "primary"
size: string = "default"
page: number = 1
pageSize: number = 10
total: number = 0
variant: string = "compact"
siblingCount: number = 1
showSummary: boolean = true
label: string = "Pagination"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
previousLabel: string = "Previous"
nextLabel: string = "Next"
class: string = ""
}
functions {
shared function lastPage() {
var size = Number(pageSize) > 0 ? Number(pageSize) : 10
var count = Number(total) > 0 ? Number(total) : 0
return Math.max(1, Math.ceil(count / size))
}
// Out-of-range values arrive routinely: page travels as an HTML attribute
// and callers compute it from data that may have shrunk underneath them.
shared function currentPage() {
var value = Number(page)
if (!value || value < 1) {
return 1
}
return Math.min(value, lastPage())
}
shared function firstShown() {
if (Number(total) < 1) {
return 0
}
return (currentPage() - 1) * (Number(pageSize) || 10) + 1
}
shared function lastShown() {
return Math.min(currentPage() * (Number(pageSize) || 10), Number(total) || 0)
}
shared function pageNumbers() {
var last = lastPage()
var current = currentPage()
var siblings = Math.max(0, Number(siblingCount) || 0)
var start = Math.max(1, current - siblings)
var end = Math.min(last, current + siblings)
var pages = []
if (start > 1) {
pages.push({ value: 1, label: "1", gap: false })
if (start > 2) {
pages.push({ value: 0, label: "...", gap: true })
}
}
for (var index = start; index <= end; index += 1) {
pages.push({ value: index, label: String(index), gap: false })
}
if (end < last) {
if (end < last - 1) {
pages.push({ value: 0, label: "...", gap: true })
}
pages.push({ value: last, label: String(last), gap: false })
}
return pages
}
client function goToPage(target) {
var next = Math.min(Math.max(1, Number(target) || 1), lastPage())
output.change({ page: next, pageSize: Number(pageSize) || 10 })
}
client function goPrevious() {
var target = Math.max(1, currentPage() - 1)
output.previous({ page: target })
output.change({ page: target, pageSize: Number(pageSize) || 10 })
}
client function goNext() {
var target = Math.min(lastPage(), currentPage() + 1)
output.next({ page: target })
output.change({ page: target, pageSize: Number(pageSize) || 10 })
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--pagination wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<nav
{...attrs}
data-ui-component="Pagination"
class='wire-pagination {class}'
data-variant='{variant}'
data-color='{color}'
data-size='{size}'
role="navigation"
aria-label='{label}'
>
<p class="wire-pagination__summary" data-show="showSummary">
{firstShown()} to {lastShown()} of {total}
</p>
<div class="wire-pagination__controls">
<button
type="button"
class="wire-pagination__step"
aria-label='{previousLabel}'
@click='goPrevious()'
>
<span class="wire-pagination__step-icon" aria-hidden="true">&#8249;</span>
<span class="wire-pagination__step-label">{previousLabel}</span>
</button>
<ol class="wire-pagination__pages" data-show="variant === 'numbered'">
{#each pageNumbers() as entry}
<li class="wire-pagination__slot">
<span class="wire-pagination__gap" data-show="entry.gap">{entry.label}</span>
<button
type="button"
class="wire-pagination__page"
data-show="!entry.gap"
data-active='{entry.value === currentPage()}'
aria-current='{entry.value === currentPage() ? "page" : "false"}'
@click='goToPage(entry.value)'
>
{entry.label}
</button>
</li>
{/each}
</ol>
<p class="wire-pagination__compact" data-show="variant !== 'numbered'">
{currentPage()} / {lastPage()}
</p>
<button
type="button"
class="wire-pagination__step"
aria-label='{nextLabel}'
@click='goNext()'
>
<span class="wire-pagination__step-label">{nextLabel}</span>
<span class="wire-pagination__step-icon" aria-hidden="true">&#8250;</span>
</button>
</div>
<slot />
</nav>
}
style {
.wire-pagination {
--pagination-accent: var(--wire-color-primary);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
max-width: 100%;
}
.wire-pagination[data-color="secondary"] {
--pagination-accent: var(--wire-color-secondary);
}
.wire-pagination[data-color="success"] {
--pagination-accent: var(--wire-color-success);
}
.wire-pagination[data-color="danger"] {
--pagination-accent: var(--wire-color-danger);
}
.wire-pagination[data-color="info"] {
--pagination-accent: var(--wire-color-info);
}
.wire-pagination[data-size="sm"] {
font-size: 0.82rem;
}
.wire-pagination[data-size="lg"] {
font-size: 1rem;
}
.wire-pagination__summary {
margin: 0;
color: var(--wire-color-text-muted);
font-size: 0.82rem;
}
.wire-pagination__controls {
display: flex;
align-items: center;
gap: 0.35rem;
flex-wrap: wrap;
}
.wire-pagination__pages {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.wire-pagination__slot {
display: inline-flex;
}
.wire-pagination__page,
.wire-pagination__step {
appearance: none;
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-width: 2.25rem;
justify-content: center;
padding: 0.4rem 0.6rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface);
color: var(--wire-color-text);
font: inherit;
font-size: 0.85rem;
cursor: pointer;
}
.wire-pagination__page:hover,
.wire-pagination__step:hover {
background: var(--wire-color-surface-soft);
}
.wire-pagination__page:focus-visible,
.wire-pagination__step:focus-visible {
outline: 2px solid var(--pagination-accent);
outline-offset: 2px;
}
.wire-pagination__page[data-active="true"] {
border-color: var(--pagination-accent);
background: var(--pagination-accent);
color: var(--wire-color-primary-contrast);
font-weight: 700;
}
.wire-pagination__gap {
padding: 0 0.35rem;
color: var(--wire-color-text-muted);
}
.wire-pagination__compact {
margin: 0;
padding: 0 0.5rem;
color: var(--wire-color-text-muted);
font-size: 0.85rem;
}
/* Below the small breakpoint the word labels crowd the arrows out. */
@media (max-width: 639px) {
.wire-pagination {
justify-content: center;
}
.wire-pagination__step-label {
display: none;
}
.wire-pagination__summary {
width: 100%;
text-align: center;
}
}
}
}
+157 -6
View File
@@ -1,21 +1,172 @@
// Scrollspy -- a table of contents that follows the reader.
//
// <Scrollspy items='[{"label":"Overview","href":"#overview"}]' />
//
// Each href points at an element on the page. The runtime observes those
// elements and moves aria-current to the link for whichever one is in view.
//
// The runtime writes the marker straight onto the links rather than into
// component state. An IntersectionObserver callback fires long after the
// client function that registered it has returned, and a state write made
// there is dropped -- so the DOM is the only place the answer can live.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Scrollspy {
outputs {
change(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)
change(payload: { href: string; label: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Scrollspy"
size: string = "default"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
label: string = "On this page"
heading: string = ""
class: string = ""
}
functions {
shared function itemList() {
return Array.isArray(items) ? items : []
}
shared function isActive(item) {
return Boolean(item.href) && item.href === active
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--scrollspy wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<nav
{...attrs}
data-ui-component="Scrollspy"
class='wire-scrollspy {class}'
data-color='{color}'
data-size='{size}'
data-wrn-scrollspy="true"
role="navigation"
aria-label='{label}'
>
<p class="wire-scrollspy__heading" data-show="heading">{heading}</p>
<ul class="wire-scrollspy__list">
{#each itemList() as item}
<li class="wire-scrollspy__item">
<a
class="wire-scrollspy__link"
href='{item.href || "#"}'
data-active='{isActive(item)}'
aria-current='{isActive(item) ? "location" : "false"}'
>
<span class="wire-scrollspy__label">{item.label}</span>
</a>
</li>
{/each}
</ul>
<slot />
</nav>
}
style {
.wire-scrollspy {
--scrollspy-accent: var(--wire-color-primary);
max-width: 100%;
}
.wire-scrollspy[data-color="secondary"] {
--scrollspy-accent: var(--wire-color-secondary);
}
.wire-scrollspy[data-color="success"] {
--scrollspy-accent: var(--wire-color-success);
}
.wire-scrollspy[data-color="danger"] {
--scrollspy-accent: var(--wire-color-danger);
}
.wire-scrollspy[data-color="info"] {
--scrollspy-accent: var(--wire-color-info);
}
.wire-scrollspy[data-size="sm"] {
font-size: 0.82rem;
}
.wire-scrollspy[data-size="lg"] {
font-size: 1rem;
}
.wire-scrollspy__heading {
margin: 0 0 0.5rem;
color: var(--wire-color-text-muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.wire-scrollspy__list {
display: grid;
gap: 0.1rem;
margin: 0;
padding: 0;
list-style: none;
border-left: 1px solid var(--wire-color-border);
}
.wire-scrollspy__link {
display: block;
padding: 0.3rem 0.75rem;
margin-left: -1px;
border-left: 2px solid transparent;
color: var(--wire-color-text-muted);
font-size: 0.85rem;
text-decoration: none;
}
.wire-scrollspy__link:hover {
color: var(--wire-color-text);
}
.wire-scrollspy__link:focus-visible {
outline: 2px solid var(--scrollspy-accent);
outline-offset: -2px;
}
.wire-scrollspy__link[data-active="true"] {
border-left-color: var(--scrollspy-accent);
color: var(--scrollspy-accent);
font-weight: 600;
}
/*
* A table of contents is a sidebar affordance. On a phone it stops being
* a rail and becomes a horizontal strip that scrolls, so it costs one
* line rather than a screenful.
*/
@media (max-width: 767px) {
.wire-scrollspy__list {
grid-auto-flow: column;
grid-auto-columns: max-content;
overflow-x: auto;
border-left: 0;
border-bottom: 1px solid var(--wire-color-border);
}
.wire-scrollspy__link {
margin-left: 0;
border-left: 0;
border-bottom: 2px solid transparent;
white-space: nowrap;
}
.wire-scrollspy__link[data-active="true"] {
border-left-color: transparent;
border-bottom-color: var(--scrollspy-accent);
}
}
}
}
+7 -2
View File
@@ -1,4 +1,9 @@
component SearchBox {
outputs {
search(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
clear(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -22,7 +27,7 @@ component SearchBox {
data-ui-component="SearchBox"
role="search"
class='w-full {class}'
@submit='event.preventDefault(); event.currentTarget.dispatchEvent(new CustomEvent("search", { bubbles: true, detail: { value: query, name: name } }))'
@submit='event.preventDefault(); output.search({ value: query, name: name })'
>
<label
class="mb-2 block text-sm font-semibold text-[var(--wire-color-text)]"
@@ -61,7 +66,7 @@ component SearchBox {
aria-label="Clear search"
data-show='query.length > 0 && !disabled'
class="absolute right-12 inline-flex size-8 items-center justify-center rounded-lg text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); event.currentTarget.dispatchEvent(new CustomEvent("clear", { bubbles: true, detail: { value: "", name: name } }))'
@click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); output.clear({ value: "", name: name })'
>
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
</button>
+302 -49
View File
@@ -1,82 +1,335 @@
// Sidebar -- a vertical navigation rail that becomes a drawer on small screens.
//
// <Sidebar items='[{"label":"Dashboard","href":"/","value":"dash"}]' active="dash" />
//
// An entry is one of three shapes:
// { label, href, value } a single link
// { heading, items: [...] } a labelled group
// { label, items: [...] } a collapsible branch, nested to 3 levels
//
// The off-canvas presentation is Drawer rather than a second implementation,
// so the focus trap and the body scroll lock come from one place.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
import Drawer from "./Drawer.wrn"
component Sidebar {
outputs {
toggle(payload: { open: boolean })
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
open(payload: { sourceEvent: Event })
close(payload: { source: string })
select(payload: { item: string | number | boolean | null | object; value: string | number | boolean; level: string })
select(payload: { item: object; value: string; level: number })
}
props {
size: string = "default"
color: string = "primary"
size: string = "default"
label: string = "Sidebar"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
mobileLabel: string = "Open navigation"
drawerTitle: string = "Navigation"
class: string = ""
}
state mobileOpen: boolean = false
state drawerOpen = false
functions {
client function toggleSidebar() {
mobileOpen = !mobileOpen
output.toggle({ open: mobileOpen })
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
shared function entryList() {
return Array.isArray(items) ? items : []
}
client function closeSidebar() {
mobileOpen = false
output.close({ source: "mobile" })
// Accepts children as well as items: children is what shipped in 0.8.5
// and existing callers should keep working.
shared function childrenOf(entry) {
if (!entry) {
return []
}
if (Array.isArray(entry.items)) {
return entry.items
}
return Array.isArray(entry.children) ? entry.children : []
}
client function selectItem(item, level) {
mobileOpen = false
output.select({ item: item, value: item.value || "", level: level })
shared function isGroup(entry) {
return Boolean(entry.heading)
}
shared function isActive(entry) {
return Boolean(entry.value) && entry.value === active
}
client function choose(entry, level) {
if (entry.disabled) {
return
}
output.select({ item: entry, value: entry.value || "", level: level })
if (drawerOpen) {
drawerOpen = false
output.close({ source: "select" })
}
}
client function openDrawer(sourceEvent) {
drawerOpen = true
output.open({ sourceEvent: sourceEvent })
output.toggle({ open: true })
}
client function closeDrawer() {
drawerOpen = false
output.close({ source: "drawer" })
output.toggle({ open: false })
}
}
view {
<div class="wire-sidebar-shell wire-next--color-{color} wire-next--size-{size} {mobileOpen ? 'is-open' : ''} {class}">
<button type="button" class="wire-sidebar-toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleSidebar()">
<span class="icon-[lucide--panel-left-open]" aria-hidden="true"></span>
<span>{label}</span>
<div
{...attrs}
data-ui-component="Sidebar"
class='wire-sidebar {class}'
data-color='{color}'
data-size='{size}'
>
<button
type="button"
class="wire-sidebar__launcher"
aria-label='{mobileLabel}'
aria-expanded='{drawerOpen}'
@click='openDrawer(event)'
>
<span class="wire-sidebar__launcher-bar" aria-hidden="true"></span>
<span class="wire-sidebar__launcher-text">{mobileLabel}</span>
</button>
<button type="button" class="wire-sidebar-backdrop" aria-label="Close navigation" @click="closeSidebar()"></button>
<nav class="wire-next wire-next--sidebar wire-next--{orientation} wire-sidebar-panel" aria-label="{label}">
<div class="wire-sidebar-panel__head">
<strong>{label}</strong>
<button type="button" aria-label="Close navigation" @click="closeSidebar()"><span class="icon-[lucide--x]" aria-hidden="true"></span></button>
</div>
<div class="wire-sidebar-items">
{#each items as item}
{#if item.children && item.children.length}
<details class="wire-sidebar-group" name="wire-sidebar-group">
<summary>
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
<span class="icon-[lucide--chevron-down]" aria-hidden="true"></span>
</summary>
<div class="wire-sidebar-group__children">
{#each item.children as child}
<a href="{child.href || '#'}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
<span>{child.label}</span>
</a>
{/each}
</div>
</details>
{:else}
<a href="{item.href || '#'}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
<nav class="wire-sidebar__rail" aria-label='{label}'>
<ul class="wire-sidebar__list" data-wrn-roving="vertical">
{#each entryList() as entry}
<li class="wire-sidebar__entry" data-group='{isGroup(entry)}'>
<p class="wire-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
<a
class="wire-sidebar__link"
data-show="!entry.heading"
href='{entry.href || "#"}'
data-wrn-roving-item="true"
data-active='{isActive(entry)}'
aria-current='{isActive(entry) ? "page" : "false"}'
aria-disabled='{entry.disabled ? "true" : "false"}'
@click='choose(entry, 1)'
>
<span
class='wire-sidebar__icon {entry.icon}'
data-show="entry.icon"
aria-hidden="true"
></span>
<span class="wire-sidebar__label">{entry.label}</span>
<span class="wire-sidebar__badge" data-show="entry.badge">{entry.badge}</span>
</a>
{/if}
<ul class="wire-sidebar__sublist" data-show="childrenOf(entry).length > 0">
{#each childrenOf(entry) as child}
<li class="wire-sidebar__entry">
<a
class="wire-sidebar__link"
href='{child.href || "#"}'
data-active='{isActive(child)}'
aria-current='{isActive(child) ? "page" : "false"}'
aria-disabled='{child.disabled ? "true" : "false"}'
@click='choose(child, 2)'
>
<span
class='wire-sidebar__icon {child.icon}'
data-show="child.icon"
aria-hidden="true"
></span>
<span class="wire-sidebar__label">{child.label}</span>
<span class="wire-sidebar__badge" data-show="child.badge">{child.badge}</span>
</a>
<ul
class="wire-sidebar__sublist wire-sidebar__sublist--level3"
data-show="childrenOf(child).length > 0"
>
{#each childrenOf(child) as leaf}
<li class="wire-sidebar__entry">
<a
class="wire-sidebar__link"
href='{leaf.href || "#"}'
data-active='{isActive(leaf)}'
aria-current='{isActive(leaf) ? "page" : "false"}'
@click='choose(leaf, 3)'
>
<span class="wire-sidebar__label">{leaf.label}</span>
</a>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
</li>
{/each}
</div>
</ul>
<slot />
</nav>
<Drawer
open={drawerOpen}
placement="left"
title={drawerTitle}
label={label}
@close="closeDrawer()"
>
<slot name="drawer"></slot>
</Drawer>
</div>
}
style {
.wire-sidebar {
--sidebar-accent: var(--wire-color-primary);
max-width: 100%;
}
.wire-sidebar[data-color="secondary"] {
--sidebar-accent: var(--wire-color-secondary);
}
.wire-sidebar[data-color="success"] {
--sidebar-accent: var(--wire-color-success);
}
.wire-sidebar[data-color="danger"] {
--sidebar-accent: var(--wire-color-danger);
}
.wire-sidebar[data-color="info"] {
--sidebar-accent: var(--wire-color-info);
}
.wire-sidebar[data-size="sm"] {
font-size: 0.82rem;
}
.wire-sidebar[data-size="lg"] {
font-size: 1rem;
}
.wire-sidebar__rail {
width: 100%;
}
.wire-sidebar__list,
.wire-sidebar__sublist {
display: grid;
gap: 0.1rem;
margin: 0;
padding: 0;
list-style: none;
}
.wire-sidebar__sublist {
margin-left: 0.85rem;
padding-left: 0.5rem;
border-left: 1px solid var(--wire-color-border);
}
.wire-sidebar__entry[data-group="true"] + .wire-sidebar__entry {
margin-top: 0.35rem;
}
.wire-sidebar__heading {
margin: 0.9rem 0 0.35rem;
color: var(--wire-color-text-muted);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.wire-sidebar__link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.6rem;
border-radius: var(--wire-radius-sm);
color: var(--wire-color-text-muted);
font-size: 0.88rem;
font-weight: 600;
text-decoration: none;
}
.wire-sidebar__link:hover {
background: var(--wire-color-surface-soft);
color: var(--wire-color-text);
}
.wire-sidebar__link:focus-visible {
outline: 2px solid var(--sidebar-accent);
outline-offset: 2px;
}
.wire-sidebar__link[data-active="true"] {
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
color: var(--sidebar-accent);
}
.wire-sidebar__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wire-sidebar__label {
min-width: 0;
}
.wire-sidebar__badge {
margin-left: auto;
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
color: var(--sidebar-accent);
font-size: 0.72rem;
}
.wire-sidebar__launcher {
display: none;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface);
color: var(--wire-color-text);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.wire-sidebar__launcher-bar {
width: 1rem;
height: 2px;
background: currentColor;
box-shadow:
0 -5px 0 currentColor,
0 5px 0 currentColor;
}
/*
* Below the tablet breakpoint the rail is replaced by the drawer launcher.
* A permanent rail eats most of a phone screen, and Drawer already handles
* the focus trap and the scroll lock.
*/
@media (max-width: 767px) {
.wire-sidebar__launcher {
display: inline-flex;
}
.wire-sidebar__rail {
display: none;
}
}
}
}
+429 -11
View File
@@ -1,24 +1,442 @@
// Stepper -- ordered progress through a sequence.
//
// <Stepper steps='[{"label":"Account"},{"label":"Billing"}]' active={1} />
//
// Each step can be authored by hand instead of using the built-in body, by
// passing a slot named for its index:
//
// <Stepper steps={steps} active={1}>
// <div data-slot="step-1">...anything...</div>
// </Stepper>
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Stepper {
outputs {
change(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)
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { index: number; step: object })
back(payload: { index: number; step: object })
next(payload: { index: number; step: object })
skip(payload: { index: number; step: object })
finish(payload: { index: number; step: object })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Stepper"
items: unknown[] = []
active: string = ""
size: string = "default"
steps: unknown[] = []
active: number = 0
orientation: string = "horizontal"
clickable: boolean = false
label: string = "Progress"
// Render each step body, showing only the active one -- same contract as
// Tabs, so a wizard does not have to hand-roll the panel switching.
showPanel: boolean = false
controls: boolean = false
allowSkip: boolean = false
// Lets a form hold the step. The component never validates anything
// itself: the page owns the form, so it sets this while the step is
// incomplete and clears it once the step passes.
nextDisabled: boolean = false
backLabel: string = "Back"
nextLabel: string = "Next"
skipLabel: string = "Skip"
finishLabel: string = "Finish"
class: string = ""
}
functions {
shared function stepList() {
return Array.isArray(steps) ? steps : []
}
shared function activeIndex() {
var count = stepList().length
if (count < 1) {
return 0
}
var value = Number(active)
if (!value || value < 0) {
return 0
}
return Math.min(value, count - 1)
}
shared function statusFor(index) {
if (index < activeIndex()) {
return "complete"
}
if (index === activeIndex()) {
return "current"
}
return "upcoming"
}
// An empty orientation is how the roving runtime is told to stay out of
// the way, so a read-only stepper never takes arrow-key focus.
shared function rovingAxis() {
if (!clickable) {
return ""
}
return orientation === "vertical" ? "vertical" : "horizontal"
}
shared function isActiveStep(index) {
return index === activeIndex()
}
shared function lastIndex() {
return Math.max(0, stepList().length - 1)
}
shared function onLastStep() {
return activeIndex() >= lastIndex()
}
shared function activeStep() {
var list = stepList()
return list.length ? list[activeIndex()] : {}
}
client function goBack() {
var target = Math.max(0, activeIndex() - 1)
output.back({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function goNext() {
if (nextDisabled) {
return
}
if (onLastStep()) {
output.finish({ index: activeIndex(), step: activeStep() })
return
}
var target = Math.min(lastIndex(), activeIndex() + 1)
output.next({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function goSkip() {
var target = Math.min(lastIndex(), activeIndex() + 1)
output.skip({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function selectStep(index, step) {
if (!clickable) {
return
}
output.change({ index: index, step: step })
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--stepper wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<div
{...attrs}
data-ui-component="Stepper"
class='wire-stepper {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-clickable='{clickable}'
>
<ol
class="wire-stepper__list"
data-wrn-roving='{rovingAxis()}'
aria-label='{label}'
>
{#each stepList() as step, index}
<li
class="wire-stepper__step"
data-status='{statusFor(index)}'
aria-current='{statusFor(index) === "current" ? "step" : "false"}'
>
<button
type="button"
class="wire-stepper__button"
data-wrn-roving-item='{clickable ? "true" : "false"}'
@click='selectStep(index, step)'
>
<span class="wire-stepper__marker" aria-hidden="true">
<span class='wire-stepper__icon {step.icon}' data-show="step.icon"></span>
<span class="wire-stepper__number" data-show="!step.icon">{index + 1}</span>
</span>
<span class="wire-stepper__body">
<span class="wire-stepper__label">{step.label}</span>
<span class="wire-stepper__description" data-show="step.description">
{step.description}
</span>
</span>
</button>
<span class="wire-stepper__custom">
<slot name="step-{index}"></slot>
</span>
</li>
{/each}
</ol>
<div class="wire-stepper__panels" data-show="showPanel">
{#each stepList() as step, index}
<div
class="wire-stepper__panel"
data-show='isActiveStep(index)'
aria-hidden='{index === activeIndex() ? "false" : "true"}'
>
<h4 class="wire-stepper__panel-title" data-show="step.title">{step.title}</h4>
<p class="wire-stepper__panel-body" data-show="step.content">{step.content}</p>
<slot name="panel-{index}"></slot>
</div>
{/each}
</div>
<div class="wire-stepper__controls" data-show="controls">
<button
type="button"
class="wire-stepper__button-control wire-stepper__back"
disabled='{activeIndex() === 0}'
@click='goBack()'
>
{backLabel}
</button>
<span class="wire-stepper__controls-spacer"></span>
<button
type="button"
class="wire-stepper__button-control wire-stepper__skip"
data-show="allowSkip && !onLastStep()"
@click='goSkip()'
>
{skipLabel}
</button>
<button
type="button"
class="wire-stepper__button-control wire-stepper__next"
data-primary="true"
disabled='{nextDisabled}'
@click='goNext()'
>
{onLastStep() ? finishLabel : nextLabel}
</button>
</div>
<slot />
</nav>
</div>
}
style {
.wire-stepper {
--stepper-accent: var(--wire-color-primary);
max-width: 100%;
}
.wire-stepper__list {
display: flex;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
max-width: 100%;
}
.wire-stepper[data-color="secondary"] {
--stepper-accent: var(--wire-color-secondary);
}
.wire-stepper[data-color="success"] {
--stepper-accent: var(--wire-color-success);
}
.wire-stepper[data-color="danger"] {
--stepper-accent: var(--wire-color-danger);
}
.wire-stepper[data-color="info"] {
--stepper-accent: var(--wire-color-info);
}
.wire-stepper[data-size="sm"] {
font-size: 0.82rem;
}
.wire-stepper[data-size="lg"] {
font-size: 1rem;
}
.wire-stepper[data-orientation="vertical"] .wire-stepper__list {
flex-direction: column;
}
.wire-stepper__step {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-width: 0;
gap: 0.35rem;
}
.wire-stepper__button {
appearance: none;
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.5rem;
border: 0;
border-radius: var(--wire-radius-sm);
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: default;
}
.wire-stepper[data-clickable="true"] .wire-stepper__button {
cursor: pointer;
}
.wire-stepper[data-clickable="true"] .wire-stepper__button:hover {
background: var(--wire-color-surface-soft);
}
.wire-stepper__button:focus-visible {
outline: 2px solid var(--stepper-accent);
outline-offset: 2px;
}
.wire-stepper__marker {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 2rem;
height: 2rem;
border: 1px solid var(--wire-color-border);
border-radius: 999px;
background: var(--wire-color-surface);
font-size: 0.85rem;
font-weight: 700;
}
.wire-stepper__step[data-status="complete"] .wire-stepper__marker {
border-color: var(--stepper-accent);
background: var(--stepper-accent);
color: var(--wire-color-primary-contrast);
}
.wire-stepper__step[data-status="current"] .wire-stepper__marker {
border-color: var(--stepper-accent);
color: var(--stepper-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--stepper-accent) 22%, transparent);
}
.wire-stepper__step[data-status="upcoming"] .wire-stepper__marker {
color: var(--wire-color-text-muted);
}
.wire-stepper__body {
display: flex;
flex-direction: column;
min-width: 0;
}
.wire-stepper__label {
font-size: 0.9rem;
font-weight: 600;
}
.wire-stepper__description {
color: var(--wire-color-text-muted);
font-size: 0.78rem;
}
.wire-stepper__step[data-status="upcoming"] .wire-stepper__label {
color: var(--wire-color-text-muted);
}
.wire-stepper__panels {
margin-top: 1rem;
}
.wire-stepper__panel {
padding: 1rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface);
animation: wire-stepper-in 200ms ease both;
}
.wire-stepper__panel-title {
margin: 0 0 0.35rem;
font-size: 0.95rem;
font-weight: 700;
}
.wire-stepper__panel-body {
margin: 0;
color: var(--wire-color-text-muted);
line-height: 1.6;
}
.wire-stepper__controls {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.85rem;
}
.wire-stepper__controls-spacer {
flex: 1 1 auto;
}
.wire-stepper__button-control {
appearance: none;
padding: 0.45rem 0.9rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface);
color: var(--wire-color-text);
font: inherit;
font-size: 0.86rem;
font-weight: 600;
cursor: pointer;
}
.wire-stepper__button-control:hover:not(:disabled) {
background: var(--wire-color-surface-soft);
}
.wire-stepper__button-control:focus-visible {
outline: 2px solid var(--stepper-accent);
outline-offset: 2px;
}
.wire-stepper__button-control[data-primary="true"] {
border-color: var(--stepper-accent);
background: var(--stepper-accent);
color: var(--wire-color-primary-contrast);
}
/* A held step has to look held, or the button reads as broken. */
.wire-stepper__button-control:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@keyframes wire-stepper-in {
from {
opacity: 0;
transform: translateX(0.75rem);
}
to {
opacity: 1;
transform: none;
}
}
/* A horizontal stepper cannot stay side by side on a phone. */
@media (max-width: 639px) {
.wire-stepper__list {
flex-direction: column;
}
}
}
}
+325 -53
View File
@@ -1,90 +1,362 @@
// Tabs -- a tablist over panels.
//
// <Tabs items='[{"label":"Overview","value":"overview"}]' active="overview" />
//
// With mode="url" the selection is mirrored into a query parameter using
// history.pushState, so the panel swaps without a page load and the back
// button works. The query parameter is used rather than the hash because it
// survives a reload and does not collide with in-page anchors or Scrollspy.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Tabs {
outputs {
change(payload: { value: string; item: object; index: number })
select(payload: { value: string; item: object; index: number })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Tabs"
size: string = "default"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
mode: string = "client"
param: string = "tab"
label: string = "Tabs"
class: string = ""
}
state activeValue = active || (items[0] ? (items[0].value || items[0].id || "0") : "")
state activeValue = ""
// Which way the panel should slide in. Set on every change so the animation
// follows the direction of travel rather than always coming from one side.
state slideFrom = "forward"
functions {
shared function itemList() {
return Array.isArray(items) ? items : []
}
shared function valueOf(item, index) {
return String(item.value || item.id || index)
}
/*
* In url mode the query parameter is the source of truth, not local state.
*
* The client router owns popstate and swaps the whole page on back and
* forward, which discards component state anyway. Reading the URL means
* the right tab simply falls out of whatever render happens next, with no
* listener to lose and nothing to keep in step.
*/
shared function currentValue() {
if (mode === "url" && typeof window !== "undefined" && window.location) {
var fromUrl = new URLSearchParams(window.location.search).get(param || "tab")
return fromUrl ? fromUrl : defaultValue()
}
if (activeValue) {
return activeValue
}
return defaultValue()
}
// The selection this instance started with. The runtime falls back to it
// when the back button lands on a URL that has no tab parameter at all.
shared function defaultValue() {
if (active) {
return active
}
var list = itemList()
return list.length ? valueOf(list[0], 0) : ""
}
shared function isSelected(item, index) {
return valueOf(item, index) === currentValue()
}
shared function rovingAxis() {
return orientation === "vertical" ? "vertical" : "horizontal"
}
client function selectTab(item, index, sourceEvent) {
if (item.disabled) {
return
}
var value = valueOf(item, index)
var list = itemList()
var previous = -1
for (var scan = 0; scan < list.length; scan += 1) {
if (valueOf(list[scan], scan) === currentValue()) {
previous = scan
}
}
slideFrom = previous > index ? "back" : "forward"
activeValue = value
if (mode === "url" && window.history && window.history.pushState) {
var url = new URL(window.location.href)
url.searchParams.set(param || "tab", value)
window.history.pushState({}, "", url.toString())
}
output.change({ value: value, item: item, index: index })
output.select({ value: value, item: item, index: index })
}
}
view {
<section
{...attrs}
data-ui-component="Tabs"
class='w-full {class}'
class:flex='orientation === "vertical"'
class:items-start='orientation === "vertical"'
class:gap-6='orientation === "vertical"'
class='wire-tabs {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-slide='{slideFrom}'
data-mode='{mode}'
data-param='{param}'
>
<div
class="wire-tabs__list"
role="tablist"
aria-label='{label}'
aria-orientation='{orientation}'
class="flex min-w-0 gap-1 overflow-x-auto rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-soft)] p-1"
class:flex-col='orientation === "vertical"'
class:w-56='orientation === "vertical"'
class:shrink-0='orientation === "vertical"'
data-wrn-roving='{rovingAxis()}'
>
{#each items as item, index}
{#each itemList() as item, index}
<button
type="button"
class="wire-tabs__tab"
role="tab"
id='tab-{item.value || item.id || index}'
aria-selected='{activeValue === (item.value || item.id || String(index))}'
aria-controls='panel-{item.value || item.id || index}'
tabindex='{activeValue === (item.value || item.id || String(index)) ? "0" : "-1"}'
disabled='{item.disabled || false}'
class="inline-flex min-w-max flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold outline-none transition disabled:cursor-not-allowed disabled:opacity-50"
class:bg-[var(--wire-color-surface-raised)]='activeValue === (item.value || item.id || String(index))'
class:text-[var(--wire-color-primary)]='activeValue === (item.value || item.id || String(index))'
class:shadow-sm='activeValue === (item.value || item.id || String(index))'
class:text-[var(--wire-color-text-muted)]='activeValue !== (item.value || item.id || String(index))'
class:hover:text-[var(--wire-color-text)]='activeValue !== (item.value || item.id || String(index))'
class:justify-start='orientation === "vertical"'
class:px-3='size === "sm"'
class:py-2='size === "sm"'
class:px-5='size === "lg"'
class:py-3='size === "lg"'
@click='activeValue = item.value || item.id || String(index); event.currentTarget.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { item: item, index: index, value: activeValue } })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index, value: activeValue } }))'
data-value='{valueOf(item, index)}'
data-wrn-roving-item="true"
id='wire-tab-{valueOf(item, index)}'
aria-controls='wire-panel-{valueOf(item, index)}'
aria-selected='{isSelected(item, index) ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
@click='selectTab(item, index, event)'
>
{#if item.icon}
<span class='{item.icon + " size-4"}' aria-hidden="true"></span>
{/if}
<span>{item.label || item.title}</span>
{#if item.badge}
<span class="rounded-full bg-[var(--wire-color-primary-soft)] px-2 py-0.5 text-xs text-[var(--wire-color-primary)]">{item.badge}</span>
{/if}
<span class='wire-tabs__icon {item.icon}' data-show="item.icon" aria-hidden="true"></span>
<span class="wire-tabs__label">{item.label || item.title}</span>
<span class="wire-tabs__badge" data-show="item.badge">{item.badge}</span>
</button>
{/each}
</div>
<div class="min-w-0 flex-1 pt-4" class:pt-0='orientation === "vertical"'>
{#each items as item, index}
<div class="wire-tabs__panels">
{#each itemList() as item, index}
<div
class="wire-tabs__panel"
role="tabpanel"
id='panel-{item.value || item.id || index}'
aria-labelledby='tab-{item.value || item.id || index}'
id='wire-panel-{valueOf(item, index)}'
aria-labelledby='wire-tab-{valueOf(item, index)}'
tabindex="0"
data-show='activeValue === (item.value || item.id || String(index))'
class="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
data-show='isSelected(item, index)'
>
{#if item.title && item.title !== item.label}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{item.title}</h3>
{/if}
{#if item.description}
<p class="mt-2 leading-7 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
{#if item.content}
<div class="mt-4 text-[var(--wire-color-text)]">{item.content}</div>
{/if}
<h3 class="wire-tabs__title" data-show="item.title && item.title !== item.label">
{item.title}
</h3>
<p class="wire-tabs__description" data-show="item.description">{item.description}</p>
<div class="wire-tabs__content" data-show="item.content">{item.content}</div>
<slot name="panel-{valueOf(item, index)}"></slot>
</div>
{/each}
<slot></slot>
<slot />
</div>
</section>
}
style {
.wire-tabs {
--tabs-accent: var(--wire-color-primary);
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 100%;
}
.wire-tabs[data-color="secondary"] {
--tabs-accent: var(--wire-color-secondary);
}
.wire-tabs[data-color="success"] {
--tabs-accent: var(--wire-color-success);
}
.wire-tabs[data-color="danger"] {
--tabs-accent: var(--wire-color-danger);
}
.wire-tabs[data-color="info"] {
--tabs-accent: var(--wire-color-info);
}
.wire-tabs[data-size="sm"] {
font-size: 0.82rem;
}
.wire-tabs[data-size="lg"] {
font-size: 1rem;
}
.wire-tabs[data-orientation="vertical"] {
flex-direction: row;
align-items: flex-start;
}
.wire-tabs__list {
display: flex;
gap: 0.25rem;
min-width: 0;
padding: 0.25rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface-soft);
/* A long tablist scrolls rather than wrapping into an unusable stack. */
overflow-x: auto;
}
.wire-tabs[data-orientation="vertical"] .wire-tabs__list {
flex-direction: column;
flex: 0 0 auto;
width: 14rem;
overflow-x: visible;
}
.wire-tabs__tab {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
flex: 1 0 auto;
padding: 0.55rem 0.9rem;
border: 0;
border-radius: var(--wire-radius-sm);
background: transparent;
color: var(--wire-color-text-muted);
font: inherit;
font-size: 0.88rem;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition:
background 160ms ease,
color 160ms ease;
}
.wire-tabs[data-orientation="vertical"] .wire-tabs__tab {
justify-content: flex-start;
}
.wire-tabs__tab:hover {
color: var(--wire-color-text);
}
.wire-tabs__tab:focus-visible {
outline: 2px solid var(--tabs-accent);
outline-offset: 2px;
}
.wire-tabs__tab[aria-selected="true"] {
background: var(--wire-color-surface);
color: var(--tabs-accent);
box-shadow: var(--wire-shadow-1);
}
.wire-tabs__tab[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wire-tabs__badge {
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--tabs-accent) 16%, transparent);
color: var(--tabs-accent);
font-size: 0.72rem;
}
.wire-tabs__panels {
min-width: 0;
flex: 1 1 auto;
}
.wire-tabs__panel {
outline: none;
animation: wire-tabs-slide-forward 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.wire-tabs[data-slide="back"] .wire-tabs__panel {
animation-name: wire-tabs-slide-back;
}
/* The panels clip their own slide so it never widens the page. */
.wire-tabs__panels {
overflow-x: clip;
}
.wire-tabs__panel:focus-visible {
outline: 2px solid var(--tabs-accent);
outline-offset: 4px;
border-radius: var(--wire-radius-sm);
}
.wire-tabs__title {
margin: 0 0 0.35rem;
font-size: 1rem;
font-weight: 700;
}
.wire-tabs__description {
margin: 0;
color: var(--wire-color-text-muted);
line-height: 1.6;
}
.wire-tabs__content {
margin-top: 0.6rem;
}
@keyframes wire-tabs-slide-forward {
from {
opacity: 0;
transform: translateX(1.25rem);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes wire-tabs-slide-back {
from {
opacity: 0;
transform: translateX(-1.25rem);
}
to {
opacity: 1;
transform: none;
}
}
/* Respect a reduced-motion preference: swap instantly instead. */
@media (prefers-reduced-motion: reduce) {
.wire-tabs__panel {
animation: none;
}
}
@media (max-width: 639px) {
.wire-tabs[data-orientation="vertical"] {
flex-direction: column;
}
.wire-tabs[data-orientation="vertical"] .wire-tabs__list {
width: 100%;
flex-direction: row;
overflow-x: auto;
}
}
}
}
+5 -1
View File
@@ -1,4 +1,8 @@
component Timeline {
outputs {
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -30,7 +34,7 @@ component Timeline {
{#each items as item, index}
<li
class="relative pb-8 pl-8 last:pb-0"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
@click='output.select({ item: item, index: index })'
>
<span
class="absolute -left-[1.05rem] top-0 flex size-8 items-center justify-center rounded-full border-4 border-[var(--wire-color-background)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-sm"
+10 -27
View File
@@ -57,34 +57,17 @@ component ToggleCount {
: emptyValue
}
client function dispatchToggleEvent(sourceEvent, previousValue, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")
if (!root) {
return
client function dispatchToggleEvent(sourceEvent, previousValue, payload) {
payload = {
component: "ToggleCount",
name: name,
value: selectedValue,
previousValue: previousValue,
firstValue: firstValue,
secondValue: secondValue
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("change", true, false, {
component: "ToggleCount",
name: name,
value: selectedValue,
previousValue: previousValue,
firstValue: firstValue,
secondValue: secondValue
})
root.dispatchEvent(customEvent)
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("toggle", true, false, {
component: "ToggleCount",
name: name,
value: selectedValue,
previousValue: previousValue,
firstValue: firstValue,
secondValue: secondValue
})
root.dispatchEvent(customEvent)
output.change(payload)
output.toggle(payload)
}
client function selectValue(sourceEvent, nextValue, previousValue) {
+62 -14
View File
@@ -11,26 +11,74 @@ component Typography {
view {
<div
data-ui-component="Typography"
class='wire-typography text-[var(--wire-color-text)] {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-none='maxWidth === "full"'
class:text-sm='size === "sm"'
class:text-base='size === "default" || size === "md"'
class:text-lg='size === "lg"'
class:columns-1='columns === 1'
class:md:columns-2='columns === 2'
class:lg:columns-3='columns === 3'
class:gap-4='gap === "sm"'
class:gap-8='gap === "md"'
class:gap-12='gap === "lg"'
class='wire-typography {class}'
data-size='{size}'
data-columns='{columns}'
data-gap='{gap}'
data-max-width='{maxWidth}'
>
<slot></slot>
</div>
}
style {
.wire-typography {
color: var(--wire-color-text);
font-size: 1rem;
}
.wire-typography[data-size="sm"] {
font-size: 0.875rem;
}
.wire-typography[data-size="lg"] {
font-size: 1.125rem;
}
.wire-typography[data-max-width="md"] {
max-width: 48rem;
}
.wire-typography[data-max-width="lg"] {
max-width: 64rem;
}
.wire-typography[data-max-width="xl"] {
max-width: 80rem;
}
.wire-typography[data-max-width="full"] {
max-width: none;
}
/*
* Multi-column prose only below a comfortable reading width; a narrow
* screen split into columns is unreadable.
*/
.wire-typography[data-gap="sm"] {
column-gap: 1rem;
}
.wire-typography[data-gap="md"] {
column-gap: 2rem;
}
.wire-typography[data-gap="lg"] {
column-gap: 3rem;
}
@media (min-width: 768px) {
.wire-typography[data-columns="2"] {
column-count: 2;
}
}
@media (min-width: 1024px) {
.wire-typography[data-columns="3"] {
column-count: 3;
}
}
.wire-typography :where(h1, h2, h3, h4) {
color: var(--wire-color-text);
font-weight: 700;
+12 -11
View File
@@ -35,22 +35,23 @@ component Alert {
state visible: boolean = true
functions {
client function dispatchAlertEvent(sourceEvent, eventName, action, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-alert]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent(eventName, true, false, {
// The output name has to be written out rather than computed: outputs are
// resolved as named properties, so a dynamic key would not reach a parent
// binding. Only two names exist here, so a branch is honest and cheap.
client function dispatchAlertEvent(sourceEvent, eventName, action, payload) {
payload = {
component: "Alert",
title: title,
color: color,
variant: variant,
action: action
})
root.dispatchEvent(customEvent)
}
if (eventName === "dismiss") {
output.dismiss(payload)
} else {
output.action(payload)
}
}
client function dismissAlert(sourceEvent) {
+6 -8
View File
@@ -35,15 +35,13 @@ component Badge {
state visible: boolean = true
functions {
client function dismissBadge(sourceEvent, root, customEvent) {
// output.dismiss reaches a parent @dismiss binding; a raw dispatchEvent on
// the root does not. The runtime registers parent handlers in a registry
// that only the output proxy consults, so the CustomEvent this used to
// build bubbled past every binding and was never seen by anyone.
client function dismissBadge() {
visible = false
root = sourceEvent.currentTarget.closest("[data-wrn-badge]")
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("dismiss", true, false, {
component: "Badge",
label: label
})
root.dispatchEvent(customEvent)
output.dismiss({ component: "Badge", label: label })
}
}
+21 -61
View File
@@ -45,67 +45,27 @@ component Card {
state dismissed: boolean = false
functions {
client function dispatchCardNavigation(sourceEvent, item, index, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("navigate", true, false, {
component: "Card",
item: item,
index: index
})
root.dispatchEvent(customEvent)
// Outputs must go through output.*; a CustomEvent dispatched on the root
// bubbles past every parent @binding without being seen, because the
// runtime keeps parent handlers in a registry only the output proxy reads.
client function dispatchCardNavigation(sourceEvent, item, index) {
output.navigate({ component: "Card", item: item, index: index })
}
client function dispatchCardNavigationValue(sourceEvent, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("navigate", true, false, {
client function dispatchCardNavigationValue(sourceEvent) {
output.navigate({
component: "Card",
value: sourceEvent.currentTarget.value
})
root.dispatchEvent(customEvent)
}
client function dispatchCardHeaderAction(sourceEvent, action, index, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("action", true, false, {
component: "Card",
action: action,
index: index
})
root.dispatchEvent(customEvent)
client function dispatchCardHeaderAction(sourceEvent, action, index) {
output.action({ component: "Card", action: action, index: index })
}
client function dismissCard(sourceEvent, root, customEvent) {
client function dismissCard() {
dismissed = true
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("dismiss", true, false, {
component: "Card",
title: title
})
root.dispatchEvent(customEvent)
output.dismiss({ component: "Card", title: title })
}
}
@@ -137,8 +97,8 @@ component Card {
alt='{item.imageAlt || item.alt || ""}'
loading='{item.loading || "lazy"}'
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { item: item, index: itemIndex } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { item: item, index: itemIndex } }))'
@load='output.load({ item: item, index: itemIndex })'
@error='output.error({ item: item, index: itemIndex })'
/>
</div>
{/if}
@@ -157,7 +117,7 @@ component Card {
<a
href='{item.actionHref || item.href || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, index: itemIndex } }))'
@click='output.action({ item: item, index: itemIndex })'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
@@ -180,8 +140,8 @@ component Card {
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
@load='output.load({ src: imageSrc })'
@error='output.error({ src: imageSrc })'
/>
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
</div>
@@ -194,8 +154,8 @@ component Card {
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
@load='output.load({ src: imageSrc })'
@error='output.error({ src: imageSrc })'
/>
</div>
{/if}
@@ -323,7 +283,7 @@ component Card {
<a
href='{actionHref || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
@click='output.action({ href: actionHref, label: actionLabel })'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
@@ -340,8 +300,8 @@ component Card {
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
@load='output.load({ src: imageSrc })'
@error='output.error({ src: imageSrc })'
/>
</div>
{/if}
+140 -24
View File
@@ -1,46 +1,162 @@
// Container -- a centred, width-limited page wrapper.
//
// <Container maxWidth="lg">...</Container>
//
// It also accepts columns and gap, because it always has and applications
// depend on it. Reach for Grid when a grid is the point; reach for Container
// when the point is the reading width.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Container {
props {
size: string = "default"
color: string = "primary"
// Stays 2 to match what shipped. A published default cannot be changed
// without silently reflowing every Container already in use.
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
centered: boolean = true
class: string = ""
}
functions {
shared function columnCount() {
var value = Number(columns)
if (!value || value < 1) {
return 1
}
return Math.min(6, value)
}
}
view {
<div
{...attrs}
data-ui-component="Container"
class='wire-container {class}'
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
data-max-width='{maxWidth}'
class='mx-auto w-full {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl" || maxWidth === "wide"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:px-4='size === "compact" || size === "default"'
class:px-5='size === "comfortable"'
class:px-6='size === "spacious"'
class:sm:px-6='size !== "compact"'
class:lg:px-8='size === "default" || size === "comfortable" || size === "spacious"'
class:grid='columns > 1'
class:grid-cols-1='columns > 1'
class:sm:grid-cols-2='columns === 2 || columns === 3 || columns === 4 || columns === 5 || columns === 6'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:lg:grid-cols-5='columns === 5'
class:lg:grid-cols-6='columns === 6'
class:gap-2='gap === "xs"'
class:gap-3='gap === "sm"'
class:gap-5='gap === "md"'
class:gap-8='gap === "lg"'
class:gap-10='gap === "xl"'
data-centered='{centered}'
data-columns='{columnCount()}'
>
<slot></slot>
</div>
}
style {
.wire-container {
--container-gap: 1.25rem;
width: 100%;
min-width: 0;
padding-inline: 1rem;
}
.wire-container[data-centered="true"] {
margin-inline: auto;
}
/* Reading widths, not breakpoints: the cap is about line length. */
.wire-container[data-max-width="md"] {
max-width: 48rem;
}
.wire-container[data-max-width="lg"] {
max-width: 64rem;
}
.wire-container[data-max-width="xl"],
.wire-container[data-max-width="wide"] {
max-width: 80rem;
}
.wire-container[data-max-width="2xl"] {
max-width: 96rem;
}
.wire-container[data-max-width="full"] {
max-width: none;
}
.wire-container[data-size="compact"] {
padding-inline: 1rem;
}
.wire-container[data-size="comfortable"] {
padding-inline: 1.25rem;
}
.wire-container[data-size="spacious"] {
padding-inline: 1.5rem;
}
.wire-container[data-gap="xs"] {
--container-gap: 0.5rem;
}
.wire-container[data-gap="sm"] {
--container-gap: 0.75rem;
}
.wire-container[data-gap="lg"] {
--container-gap: 2rem;
}
.wire-container[data-gap="xl"] {
--container-gap: 2.5rem;
}
/*
* Columns are opt-in. One column stays plain flow so a container does not
* quietly turn every page into a grid.
*/
.wire-container:not([data-columns="1"]) {
display: grid;
gap: var(--container-gap);
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.wire-container[data-columns="2"],
.wire-container[data-columns="3"],
.wire-container[data-columns="4"],
.wire-container[data-columns="5"],
.wire-container[data-columns="6"] {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wire-container[data-size="default"],
.wire-container[data-size="comfortable"],
.wire-container[data-size="spacious"] {
padding-inline: 1.5rem;
}
}
@media (min-width: 1024px) {
.wire-container[data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wire-container[data-columns="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.wire-container[data-columns="5"] {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.wire-container[data-columns="6"] {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.wire-container[data-size="default"],
.wire-container[data-size="comfortable"],
.wire-container[data-size="spacious"] {
padding-inline: 2rem;
}
}
}
}
+121 -49
View File
@@ -1,69 +1,141 @@
// Divider -- a rule between sections, optionally labelled.
//
// <Divider />
// <Divider label="or" />
// <Divider orientation="vertical" />
//
// The label sits between two rules rather than on top of one, so the text
// never overlaps the line at any width.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Divider {
props {
size: string = "default"
color: string = "primary"
label: string = ""
orientation: string = "horizontal"
variant: string = "solid"
class: string = ""
}
functions {
shared function isVertical() {
return orientation === "vertical"
}
// The rule only takes the component colour when there is a label. A plain
// divider is chrome and should stay the border token.
shared function tone() {
return label ? color : "border"
}
}
view {
<div
{...attrs}
data-ui-component="Divider"
class='wire-divider {class}'
data-size='{size}'
data-color='{color}'
data-tone='{tone()}'
data-variant='{variant}'
data-orientation='{orientation}'
class='w-full {class}'
class:h-full='orientation === "vertical"'
class:w-auto='orientation === "vertical"'
data-labelled='{label ? "true" : "false"}'
role="separator"
aria-orientation='{isVertical() ? "vertical" : "horizontal"}'
>
{#if orientation === "vertical"}
<div
role="separator"
aria-orientation="vertical"
class="mx-3 h-full min-h-6 border-l border-[var(--wire-color-border)]"
class:border-l-2='size === "lg"'
class:border-[var(--wire-color-primary)]='color === "primary"'
class:border-[var(--wire-color-secondary)]='color === "secondary"'
class:border-[var(--wire-color-success)]='color === "success"'
class:border-[var(--wire-color-warning)]='color === "warning"'
class:border-[var(--wire-color-danger)]='color === "danger"'
></div>
{:else}
<div
role="separator"
aria-orientation="horizontal"
class="flex w-full items-center gap-4"
>
<span
class="h-px flex-1 bg-[var(--wire-color-border)]"
class:h-0.5='size === "lg"'
class:bg-[var(--wire-color-primary)]='color === "primary" && label !== ""'
class:bg-[var(--wire-color-secondary)]='color === "secondary" && label !== ""'
class:bg-[var(--wire-color-success)]='color === "success" && label !== ""'
class:bg-[var(--wire-color-warning)]='color === "warning" && label !== ""'
class:bg-[var(--wire-color-danger)]='color === "danger" && label !== ""'
aria-hidden="true"
></span>
{#if label}
<span class="shrink-0 text-xs font-bold uppercase tracking-[0.16em] text-[var(--wire-color-text-muted)]">
{label}
</span>
<span
class="h-px flex-1 bg-[var(--wire-color-border)]"
class:h-0.5='size === "lg"'
class:bg-[var(--wire-color-primary)]='color === "primary"'
class:bg-[var(--wire-color-secondary)]='color === "secondary"'
class:bg-[var(--wire-color-success)]='color === "success"'
class:bg-[var(--wire-color-warning)]='color === "warning"'
class:bg-[var(--wire-color-danger)]='color === "danger"'
aria-hidden="true"
></span>
{/if}
</div>
{/if}
<span class="wire-divider__rule" aria-hidden="true"></span>
<span class="wire-divider__label" data-show="label">{label}</span>
<span class="wire-divider__rule" data-show="label" aria-hidden="true"></span>
</div>
}
style {
.wire-divider {
--divider-tone: var(--wire-color-border);
--divider-thickness: 1px;
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
min-width: 0;
}
.wire-divider[data-tone="primary"] {
--divider-tone: var(--wire-color-primary);
}
.wire-divider[data-tone="secondary"] {
--divider-tone: var(--wire-color-secondary);
}
.wire-divider[data-tone="success"] {
--divider-tone: var(--wire-color-success);
}
.wire-divider[data-tone="warning"] {
--divider-tone: var(--wire-color-warning);
}
.wire-divider[data-tone="danger"] {
--divider-tone: var(--wire-color-danger);
}
.wire-divider[data-size="lg"] {
--divider-thickness: 2px;
}
.wire-divider__rule {
flex: 1 1 auto;
height: var(--divider-thickness);
background: var(--divider-tone);
}
.wire-divider[data-variant="dashed"] .wire-divider__rule {
height: 0;
background: transparent;
border-top: var(--divider-thickness) dashed var(--divider-tone);
}
.wire-divider[data-variant="dotted"] .wire-divider__rule {
height: 0;
background: transparent;
border-top: var(--divider-thickness) dotted var(--divider-tone);
}
.wire-divider__label {
flex: 0 0 auto;
color: var(--wire-color-text-muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.wire-divider[data-orientation="vertical"] {
flex-direction: column;
width: auto;
height: 100%;
min-height: 1.5rem;
margin-inline: 0.75rem;
}
.wire-divider[data-orientation="vertical"] .wire-divider__rule {
width: var(--divider-thickness);
height: auto;
}
.wire-divider[data-orientation="vertical"][data-variant="dashed"] .wire-divider__rule {
width: 0;
border-top: 0;
border-left: var(--divider-thickness) dashed var(--divider-tone);
}
.wire-divider[data-orientation="vertical"][data-variant="dotted"] .wire-divider__rule {
width: 0;
border-top: 0;
border-left: var(--divider-thickness) dotted var(--divider-tone);
}
}
}
+117 -19
View File
@@ -1,3 +1,13 @@
// Grid -- an equal-track grid that collapses on small screens.
//
// <Grid columns={3} gap="lg">...</Grid>
//
// Pass minItemWidth to let the browser decide the count instead: the track
// list becomes auto-fit, which reflows continuously rather than at fixed
// breakpoints and suits card decks whose item count is not known.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Grid {
props {
size: string = "default"
@@ -5,36 +15,124 @@ component Grid {
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
// When set, the count is derived from the available width instead of the
// columns prop. Accepts any CSS length.
minItemWidth: string = ""
class: string = ""
}
functions {
shared function columnCount() {
var value = Number(columns)
if (!value || value < 1) {
return 1
}
return Math.min(6, value)
}
shared function autoTrack() {
return minItemWidth ? "repeat(auto-fit, minmax(" + minItemWidth + ", 1fr))" : ""
}
}
view {
<div
{...attrs}
data-ui-component="Grid"
class='wire-grid {class}'
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:sm:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:lg:grid-cols-5='columns === 5'
class:lg:grid-cols-6='columns === 6'
class:gap-2='gap === "xs"'
class:gap-3='gap === "sm"'
class:gap-5='gap === "md"'
class:gap-8='gap === "lg"'
class:gap-10='gap === "xl"'
class:items-start='size === "compact"'
class:items-stretch='size !== "compact"'
data-max-width='{maxWidth}'
data-columns='{columnCount()}'
data-auto='{minItemWidth ? "true" : "false"}'
style='--grid-auto-track:{autoTrack()};'
>
<slot></slot>
</div>
}
style {
.wire-grid {
--grid-gap: 1.25rem;
display: grid;
width: 100%;
min-width: 0;
gap: var(--grid-gap);
grid-template-columns: 1fr;
align-items: stretch;
}
.wire-grid[data-size="compact"] {
align-items: start;
}
.wire-grid[data-max-width="md"] {
max-width: 48rem;
}
.wire-grid[data-max-width="lg"] {
max-width: 64rem;
}
.wire-grid[data-max-width="xl"] {
max-width: 80rem;
}
.wire-grid[data-max-width="2xl"] {
max-width: 96rem;
}
.wire-grid[data-max-width="full"] {
max-width: none;
}
.wire-grid[data-gap="xs"] {
--grid-gap: 0.5rem;
}
.wire-grid[data-gap="sm"] {
--grid-gap: 0.75rem;
}
.wire-grid[data-gap="lg"] {
--grid-gap: 2rem;
}
.wire-grid[data-gap="xl"] {
--grid-gap: 2.5rem;
}
/*
* auto-fit needs no breakpoints, so it wins over the fixed counts below
* and applies at every width.
*/
.wire-grid[data-auto="true"] {
grid-template-columns: var(--grid-auto-track);
}
@media (min-width: 640px) {
.wire-grid[data-auto="false"]:not([data-columns="1"]) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.wire-grid[data-auto="false"][data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wire-grid[data-auto="false"][data-columns="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.wire-grid[data-auto="false"][data-columns="5"] {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.wire-grid[data-auto="false"][data-columns="6"] {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT",
@@ -46,7 +46,7 @@
"./ui.css": "./ui.css"
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
"@wrnexus/core": "^0.8.6"
},
"files": [
"dist",
-2012
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Secure upload drivers, policies, client runtime, helper functions, and reusable upload components.",
"license": "MIT",
@@ -42,9 +42,9 @@
"./components/*": "./components/*"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
"@wrnexus/core": "^0.8.6",
"@wrnexus/plugin": "^0.8.6",
"@wrnexus/ui": "^0.8.6"
},
"wrnexus": {
"plugin": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Shared server/browser schemas, validation helpers, form runtime, and reusable error components.",
"license": "MIT",
@@ -42,9 +42,9 @@
"./components/*": "./components/*"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
"@wrnexus/core": "^0.8.6",
"@wrnexus/plugin": "^0.8.6",
"@wrnexus/ui": "^0.8.6"
},
"wrnexus": {
"plugin": {
+47 -47
View File
@@ -107,11 +107,11 @@
},
"packages/ai": {
"name": "@wrnexus/ai",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/auth": {
"name": "@wrnexus/auth",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*",
@@ -133,7 +133,7 @@
},
"packages/authz": {
"name": "@wrnexus/authz",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
@@ -141,18 +141,18 @@
},
"packages/benchmark": {
"name": "@wrnexus/benchmark",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/cache": {
"name": "@wrnexus/cache",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/captcha": {
"name": "@wrnexus/captcha",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -167,7 +167,7 @@
},
"packages/cli": {
"name": "@wrnexus/cli",
"version": "0.8.5",
"version": "0.8.6",
"bin": {
"wrnexus": "src/index.ts",
},
@@ -194,7 +194,7 @@
},
"packages/compiler": {
"name": "@wrnexus/compiler",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*",
@@ -204,7 +204,7 @@
},
"packages/content": {
"name": "@wrnexus/content",
"version": "0.8.5",
"version": "0.8.6",
"devDependencies": {
"@types/bun": "^1.3.14",
"typescript": "^5.9.2",
@@ -212,18 +212,18 @@
},
"packages/core": {
"name": "@wrnexus/core",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/csr": {
"name": "@wrnexus/csr",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/db": {
"name": "@wrnexus/db",
"version": "0.8.5",
"version": "0.8.6",
"devDependencies": {
"@types/bun": "^1.3.14",
"typescript": "^5.9.2",
@@ -231,7 +231,7 @@
},
"packages/dev-server": {
"name": "@wrnexus/dev-server",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*",
@@ -258,7 +258,7 @@
},
"packages/dev-toolbar": {
"name": "@wrnexus/dev-toolbar",
"version": "0.8.5",
"version": "0.8.6",
"devDependencies": {
"@types/bun": "^1.3.14",
"typescript": "^5.9.2",
@@ -266,7 +266,7 @@
},
"packages/encryption": {
"name": "@wrnexus/encryption",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
@@ -277,21 +277,21 @@
},
"packages/graphql": {
"name": "@wrnexus/graphql",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/plugin": "workspace:*",
},
},
"packages/helpers": {
"name": "@wrnexus/helpers",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/i18n": {
"name": "@wrnexus/i18n",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -305,7 +305,7 @@
},
"packages/identity": {
"name": "@wrnexus/identity",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/auth": "workspace:*",
"@wrnexus/authz": "workspace:*",
@@ -314,7 +314,7 @@
},
"packages/image": {
"name": "@wrnexus/image",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/plugin": "workspace:*",
"@wrnexus/security": "workspace:*",
@@ -334,7 +334,7 @@
},
"packages/jwt": {
"name": "@wrnexus/jwt",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
@@ -345,7 +345,7 @@
},
"packages/language-server": {
"name": "@wrnexus/language-server",
"version": "0.8.5",
"version": "0.8.6",
"bin": {
"wrnexus-language-server": "src/server.ts",
},
@@ -356,39 +356,39 @@
},
"packages/mcp": {
"name": "@wrnexus/mcp",
"version": "0.8.5",
"version": "0.8.6",
"bin": {
"wrnexus-mcp": "src/stdio.ts",
},
},
"packages/mobile": {
"name": "@wrnexus/mobile",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/native": "workspace:*",
},
},
"packages/native": {
"name": "@wrnexus/native",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/oauth": {
"name": "@wrnexus/oauth",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/jwt": "workspace:*",
},
},
"packages/observability": {
"name": "@wrnexus/observability",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/playground": {
"name": "@wrnexus/playground",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/compiler": "workspace:*",
"@wrnexus/syntax": "workspace:*",
@@ -396,18 +396,18 @@
},
"packages/plugin": {
"name": "@wrnexus/plugin",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/syntax": "workspace:*",
},
},
"packages/pubsub": {
"name": "@wrnexus/pubsub",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/pwa": {
"name": "@wrnexus/pwa",
"version": "0.8.5",
"version": "0.8.6",
"devDependencies": {
"@types/bun": "^1.3.14",
"typescript": "^5.9.2",
@@ -415,18 +415,18 @@
},
"packages/queue": {
"name": "@wrnexus/queue",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/reactive": {
"name": "@wrnexus/reactive",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/realtime": {
"name": "@wrnexus/realtime",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -440,7 +440,7 @@
},
"packages/router": {
"name": "@wrnexus/router",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/compiler": "workspace:*",
"@wrnexus/core": "workspace:*",
@@ -448,7 +448,7 @@
},
"packages/rpc": {
"name": "@wrnexus/rpc",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*",
@@ -463,14 +463,14 @@
},
"packages/security": {
"name": "@wrnexus/security",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/ssr": {
"name": "@wrnexus/ssr",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/security": "workspace:*",
@@ -479,11 +479,11 @@
},
"packages/store": {
"name": "@wrnexus/store",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/styles": {
"name": "@wrnexus/styles",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -492,19 +492,19 @@
},
"packages/syntax": {
"name": "@wrnexus/syntax",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/test": {
"name": "@wrnexus/test",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/tracking": {
"name": "@wrnexus/tracking",
"version": "0.8.5",
"version": "0.8.6",
},
"packages/typecheck": {
"name": "@wrnexus/typecheck",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/syntax": "workspace:*",
"typescript": "^5.5.0",
@@ -512,14 +512,14 @@
},
"packages/ui": {
"name": "@wrnexus/ui",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/uploader": {
"name": "@wrnexus/uploader",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -533,7 +533,7 @@
},
"packages/validation": {
"name": "@wrnexus/validation",
"version": "0.8.5",
"version": "0.8.6",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "wrnexus",
"version": "0.8.5",
"version": "0.8.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrnexus",
"version": "0.8.5",
"version": "0.8.6",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"vscode-languageclient": "^10.1.0"
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
"version": "0.8.5",
"version": "0.8.6",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
+1 -1
View File
@@ -1,4 +1,4 @@
// WRN editor extension source hash: 6afaf7958331407986761b491ad81d56458dcb162f56369a4cc29f86ff95d981
// WRN editor extension source hash: 6165420f37af57e441086efeb6b25528f72e77a39abb2c76ca5bc8748dd90c2f
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
"use strict";
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// WRN editor language server source hash: c4362f7edf117e0c403c9bce9435bc02ae0febd963edb4af9a18d2d74d87eb47
// WRN editor language server source hash: 65a5536efbc7c9fee15040559bb48afe78c4686fc5e7341ffc941cc7113c8556
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wrnexus",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.8.5",
"version": "0.8.6",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/benchmark",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cache",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
"version": "0.8.5",
"version": "0.8.6",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/content",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/graphql",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "./src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/identity",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Enterprise federation, provisioning, machine identity, and privacy governance for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/image",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/language-server",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Editor-neutral Language Server Protocol implementation for WRNexus .wrn files.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mcp",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Model Context Protocol server exposing WRNexus application and framework context.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/observability",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Metrics, Web Vitals collection, request instrumentation, and exporter adapters for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/playground",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Secure, shareable WRNexus compiler and UI playground.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.8.5",
"version": "0.8.6",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pwa",
"version": "0.8.5",
"version": "0.8.6",
"type": "module",
"description": "Service workers, manifests, offline queues, background sync, push, and conflict resolution for WRNexusJS.",
"main": "./src/index.ts",

Some files were not shown because too many files have changed in this diff Show More