release: WRNexusJS 0.5.12

This commit is contained in:
2026-07-30 18:59:01 +05:30
parent f37303b22d
commit d64e899993
462 changed files with 75432 additions and 2570 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT",
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT",
@@ -28,18 +28,18 @@
"wrnexus": "./dist/index.js"
},
"dependencies": {
"@wrnexus/core": "^0.5.11",
"@wrnexus/router": "^0.5.11",
"@wrnexus/csr": "^0.5.11",
"@wrnexus/compiler": "^0.5.11",
"@wrnexus/styles": "^0.5.11",
"@wrnexus/dev-server": "^0.5.11",
"@wrnexus/ui": "^0.5.11",
"@wrnexus/validation": "^0.5.11",
"@wrnexus/i18n": "^0.5.11",
"@wrnexus/db": "^0.5.11",
"@wrnexus/plugin": "^0.5.11",
"@wrnexus/syntax": "^0.5.11"
"@wrnexus/core": "^0.5.12",
"@wrnexus/router": "^0.5.12",
"@wrnexus/csr": "^0.5.12",
"@wrnexus/compiler": "^0.5.12",
"@wrnexus/styles": "^0.5.12",
"@wrnexus/dev-server": "^0.5.12",
"@wrnexus/ui": "^0.5.12",
"@wrnexus/validation": "^0.5.12",
"@wrnexus/i18n": "^0.5.12",
"@wrnexus/db": "^0.5.12",
"@wrnexus/plugin": "^0.5.12",
"@wrnexus/syntax": "^0.5.12"
},
"files": [
"dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/syntax": "^0.5.11"
"@wrnexus/syntax": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.5.11",
"version": "0.5.12",
"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.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.5.11"
"@wrnexus/core": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT",
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT",
@@ -25,20 +25,20 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.5.11",
"@wrnexus/dev-toolbar": "^0.5.11",
"@wrnexus/router": "^0.5.11",
"@wrnexus/ssr": "^0.5.11",
"@wrnexus/csr": "^0.5.11",
"@wrnexus/compiler": "^0.5.11",
"@wrnexus/styles": "^0.5.11",
"@wrnexus/ui": "^0.5.11",
"@wrnexus/validation": "^0.5.11",
"@wrnexus/i18n": "^0.5.11",
"@wrnexus/db": "^0.5.11",
"@wrnexus/pubsub": "^0.5.11",
"@wrnexus/uploader": "^0.5.11",
"@wrnexus/plugin": "^0.5.11"
"@wrnexus/core": "^0.5.12",
"@wrnexus/dev-toolbar": "^0.5.12",
"@wrnexus/router": "^0.5.12",
"@wrnexus/ssr": "^0.5.12",
"@wrnexus/csr": "^0.5.12",
"@wrnexus/compiler": "^0.5.12",
"@wrnexus/styles": "^0.5.12",
"@wrnexus/ui": "^0.5.12",
"@wrnexus/validation": "^0.5.12",
"@wrnexus/i18n": "^0.5.12",
"@wrnexus/db": "^0.5.12",
"@wrnexus/pubsub": "^0.5.12",
"@wrnexus/uploader": "^0.5.12",
"@wrnexus/plugin": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.5.11"
"@wrnexus/core": "^0.5.12"
},
"files": [
"dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.5.11"
"@wrnexus/core": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/native": "^0.5.11"
"@wrnexus/native": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT",
@@ -33,7 +33,7 @@
}
},
"dependencies": {
"@wrnexus/syntax": "^0.5.11"
"@wrnexus/syntax": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.5.11",
"version": "0.5.12",
"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.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT",
@@ -21,8 +21,8 @@
}
},
"dependencies": {
"@wrnexus/compiler": "^0.5.11",
"@wrnexus/core": "^0.5.11"
"@wrnexus/compiler": "^0.5.12",
"@wrnexus/core": "^0.5.12"
},
"files": [
"dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.5.11"
"@wrnexus/core": "^0.5.12"
},
"files": [
"dist"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT",
@@ -21,9 +21,9 @@
}
},
"dependencies": {
"@wrnexus/uploader": "^0.5.11",
"@wrnexus/core": "^0.5.11",
"@wrnexus/plugin": "^0.5.11"
"@wrnexus/uploader": "^0.5.12",
"@wrnexus/core": "^0.5.12",
"@wrnexus/plugin": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.5.11",
"version": "0.5.12",
"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.5.11",
"version": "0.5.12",
"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.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT",
+171 -9
View File
@@ -1,6 +1,6 @@
# WRNexus UI component reference
This reference is generated from the 90 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
This reference is generated from the 108 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
## Advanced-forms
@@ -56,7 +56,7 @@ Theme-aware, responsive search box component.
- 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
- Events: `input`, `change`, `search`, `submit`, `clear`
- Events: None
### StrongPassword
@@ -222,6 +222,15 @@ Theme-aware, responsive file upload progress component.
- Slots: None
- Events: `cancel`, `retry`, `complete`
### HeroActions
Responsive action group for hero and call-to-action sections.
- Mount: `data-component="HeroActions"`
- Props: `actions: string = []`, `align: string = "left"`, `orientation: string = "horizontal"`, `stackOnMobile: boolean = true`, `fullWidthMobile: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
- Slots: `default`
- Events: None
### LegendIndicator
Theme-aware, responsive legend indicator component.
@@ -238,7 +247,7 @@ Theme-aware, responsive list component.
- Mount: `data-component="List"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `select`, `click`
- Events: None
### ListGroup
@@ -256,7 +265,7 @@ Theme-aware, responsive marquee component.
- Mount: `data-component="Marquee"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `pause`, `resume`
- Events: None
### Progress
@@ -310,7 +319,7 @@ Theme-aware, responsive timeline component.
- Mount: `data-component="Timeline"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `select`
- Events: None
### Toast
@@ -332,6 +341,78 @@ Theme-aware, responsive tree view component.
## Blocks
### AnnouncementBar
Accessible public announcement, emergency, maintenance, or status bar.
- Mount: `data-component="AnnouncementBar"`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Slots: None
- Events: None
### CTASection
Reusable closing call-to-action section with primary and secondary actions.
- Mount: `data-component="CTASection"`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`, `actions`, `visual`, `footer`
- Events: None
### FeatureCard
Reusable linked feature or service card with icon, badge, description, and action.
- Mount: `data-component="FeatureCard"`
- Props: `icon: string = ""`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Learn more"`, `actionIcon: string = ""`, `badge: string = ""`, `badgeColor: string = "primary"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `disabled: boolean = false`, `class: string = ""`
- Slots: `icon`, `default`, `footer`
- Events: None
### FeatureIconCard
Feature card with a prominent themed icon treatment.
- Mount: `data-component="FeatureIconCard"`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
- Slots: `default`, `footer`
- Events: None
### Hero
Reusable public-page hero with actions, trust items, badges, and visual slots.
- Mount: `data-component="Hero"`
- Props: `eyebrow: string = ""`, `title: string = "Build something remarkable"`, `highlight: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `tertiaryLabel: string = ""`, `tertiaryHref: string = ""`, `badges: string = []`, `trustItems: string = []`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `eyebrow`, `actions`, `trust`, `default`, `visual`, `footer`
- Events: None
### MarketingSectionHeader
Marketing section heading with an optional linked action.
- Mount: `data-component="MarketingSectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "split"`, `size: string = "default"`, `color: string = "primary"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `actionExternal: boolean = false`, `class: string = ""`
- Slots: `icon`, `actions`, `default`
- Events: None
### MetricCard
Single statistic or operational metric with icon, trend, and optional action.
- Mount: `data-component="MetricCard"`
- Props: `label: string = "Metric"`, `value: string = "0"`, `description: string = ""`, `icon: string = ""`, `prefix: string = ""`, `suffix: string = ""`, `trend: string = ""`, `trendLabel: string = ""`, `trendDirection: string = "neutral"`, `href: string = ""`, `actionLabel: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `class: string = ""`
- Slots: None
- Events: None
### PageHeader
Public and application page header with breadcrumbs, metadata, and actions.
- Mount: `data-component="PageHeader"`
- Props: `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `align: string = "left"`, `size: string = "default"`, `compact: boolean = false`, `showBreadcrumbs: boolean = false`, `breadcrumbs: string = []`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `meta`, `actions`, `default`
- Events: None
### PortalDashboard
Responsive portal dashboard with metrics, actions, tasks, and updates.
@@ -341,6 +422,24 @@ Responsive portal dashboard with metrics, actions, tasks, and updates.
- Slots: `hero-action`
- Events: `action`, `navigate`
### SplitHero
Two-column hero with configurable content and visual placement.
- Mount: `data-component="SplitHero"`
- Props: `eyebrow: string = ""`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `trustItems: string = []`, `class: string = ""`
- Slots: `actions`, `trust`, `default`, `visual`
- Events: None
### StatsBar
Compact responsive statistics strip.
- Mount: `data-component="StatsBar"`
- Props: `items: string = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: None
- Events: None
## Forms
### AuthForm
@@ -541,7 +640,7 @@ Theme-aware, responsive map component.
- Mount: `data-component="Map"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `ready`, `move`, `zoom`, `markerClick`
- Events: None
### ToastNotifications
@@ -608,6 +707,15 @@ Theme-aware, responsive divider component.
- Slots: None
- Events: None
### FeatureGrid
Responsive equal-height grid for feature and service cards.
- Mount: `data-component="FeatureGrid"`
- Props: `color: string = "primary"`, `size: string = "default"`, `columns: number = 3`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `minItemWidth: string = ""`, `equalHeight: boolean = true`, `align: string = "stretch"`, `maxWidth: string = "full"`, `class: string = ""`
- Slots: `default`
- Events: None
### Grid
Theme-aware, responsive grid component.
@@ -624,7 +732,7 @@ Theme-aware, responsive image component.
- 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 = ""`
- Slots: `default`
- Events: `load`, `error`
- Events: None
### Kbd
@@ -651,6 +759,51 @@ Theme-aware, responsive link component.
- 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 = ""`
- Slots: `default`
- Events: None
### MetricGrid
Responsive collection of metric cards.
- Mount: `data-component="MetricGrid"`
- Props: `items: string = []`, `columns: number = 4`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `equalHeight: boolean = true`, `dividers: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: None
### PublicPageShell
Consistent structural wrapper for public-facing pages.
- Mount: `data-component="PublicPageShell"`
- Props: `maxWidth: string = "full"`, `fullWidth: boolean = true`, `headerOffset: string = "none"`, `background: string = "default"`, `overflow: string = "clip"`, `minHeight: string = "screen"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `before`, `default`, `after`
- Events: None
### Section
Theme-aware page section with consistent spacing, width, background, and border controls.
- Mount: `data-component="Section"`
- Props: `id: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `spacing: string = "lg"`, `maxWidth: string = "xl"`, `fullWidth: boolean = false`, `borderTop: boolean = false`, `borderBottom: boolean = false`, `class: string = ""`
- Slots: `default`
- Events: None
### SectionHeader
Reusable accessible section heading with eyebrow, title, description, icon, and actions.
- Mount: `data-component="SectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `headingLevel: number = 2`, `maxWidth: string = "3xl"`, `class: string = ""`
- Slots: `icon`, `default`, `actions`
- Events: None
### TextLink
Styled inline action link with icons, arrows, external state, and disabled handling.
- Mount: `data-component="TextLink"`
- Props: `label: string = "Learn more"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `showArrow: boolean = true`, `underline: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `disabled: boolean = false`, `class: string = ""`
- Slots: None
- Events: `click`, `focus`, `blur`
### Typography
@@ -664,6 +817,15 @@ Theme-aware, responsive typography component.
## Navigation
### BackToTop
Accessible scroll-to-top control with visibility threshold.
- Mount: `data-component="BackToTop"`
- Props: `threshold: number = 500`, `label: string = "Back to top"`, `ariaLabel: string = "Scroll back to top"`, `icon: string = "icon-[lucide--arrow-up]"`, `position: string = "right"`, `offset: string = "md"`, `behavior: string = "smooth"`, `showProgress: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `class: string = ""`
- Slots: None
- Events: None
### Breadcrumb
Theme-aware, responsive breadcrumb component.
@@ -679,7 +841,7 @@ Responsive application footer with structured links and pre/post content slots.
- Mount: `data-component="Footer"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: string = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
- Slots: `pre-footer`, `copyright-left`, `copyright-right`, `post-footer`
- Events: `select`, `action`
### MegaMenu
@@ -761,7 +923,7 @@ Theme-aware, responsive tabs component.
- Mount: `data-component="Tabs"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Tabs"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Events: `change`, `select`
- Events: None
## Overlays
+412 -322
View File
@@ -1,66 +1,47 @@
{
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
"components": [
{
"name": "Container",
"category": "layout",
"purpose": "Theme-aware, responsive container component."
},
{
"name": "Columns",
"category": "layout",
"purpose": "Theme-aware, responsive columns component."
},
{
"name": "Grid",
"category": "layout",
"purpose": "Theme-aware, responsive grid component."
},
{
"name": "LayoutSplitter",
"category": "layout",
"purpose": "Theme-aware, responsive layout splitter component."
},
{
"name": "Typography",
"category": "layout",
"purpose": "Theme-aware, responsive typography component."
},
{
"name": "Image",
"category": "layout",
"purpose": "Theme-aware, responsive image component."
},
{
"name": "Link",
"category": "layout",
"purpose": "Theme-aware, responsive link component."
},
{
"name": "Divider",
"category": "layout",
"purpose": "Theme-aware, responsive divider component."
},
{
"name": "Kbd",
"category": "layout",
"purpose": "Theme-aware, responsive kbd component."
},
{
"name": "CustomScrollbar",
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component."
},
{
"name": "Accordion",
"category": "base",
"purpose": "Theme-aware, responsive accordion component."
},
{
"name": "AdvancedDatePicker",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced date picker component."
},
{
"name": "AdvancedRangeSlider",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced range slider component."
},
{
"name": "AdvancedSelect",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive advanced select component.",
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
},
{
"name": "Alert",
"category": "base",
"purpose": "Theme-aware, responsive alert component."
},
{
"name": "AnnouncementBar",
"category": "blocks",
"purpose": "Accessible public announcement, emergency, maintenance, or status bar."
},
{
"name": "AuthForm",
"category": "forms",
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA."
},
{
"name": "AuthSplitLayout",
"category": "layout",
"purpose": "Responsive 50/50 authentication layout with content and form regions."
},
{
"name": "Avatar",
"category": "base",
@@ -71,6 +52,11 @@
"category": "base",
"purpose": "Theme-aware, responsive avatar group component."
},
{
"name": "BackToTop",
"category": "navigation",
"purpose": "Accessible scroll-to-top control with visibility threshold."
},
{
"name": "Badge",
"category": "base",
@@ -81,6 +67,11 @@
"category": "base",
"purpose": "Theme-aware, responsive blockquote component."
},
{
"name": "Breadcrumb",
"category": "navigation",
"purpose": "Theme-aware, responsive breadcrumb component."
},
{
"name": "Button",
"category": "base",
@@ -96,165 +87,20 @@
"category": "base",
"purpose": "Theme-aware, responsive card component."
},
{
"name": "ChatBubble",
"category": "base",
"purpose": "Theme-aware, responsive chat bubble component."
},
{
"name": "Carousel",
"category": "base",
"purpose": "Theme-aware, responsive carousel component."
},
{
"name": "Collapse",
"name": "Chart",
"category": "integrations",
"purpose": "Theme-aware, responsive chart component."
},
{
"name": "ChatBubble",
"category": "base",
"purpose": "Theme-aware, responsive collapse component."
},
{
"name": "DatePicker",
"category": "base",
"purpose": "Theme-aware, responsive date picker component."
},
{
"name": "DeviceFrame",
"category": "base",
"purpose": "Theme-aware, responsive device frame component."
},
{
"name": "List",
"category": "base",
"purpose": "Theme-aware, responsive list component."
},
{
"name": "ListGroup",
"category": "base",
"purpose": "Theme-aware, responsive list group component."
},
{
"name": "LegendIndicator",
"category": "base",
"purpose": "Theme-aware, responsive legend indicator component."
},
{
"name": "Progress",
"category": "base",
"purpose": "Theme-aware, responsive progress component."
},
{
"name": "FileUploadProgress",
"category": "base",
"purpose": "Theme-aware, responsive file upload progress component."
},
{
"name": "Rating",
"category": "base",
"purpose": "Theme-aware, responsive rating component."
},
{
"name": "Skeleton",
"category": "base",
"purpose": "Theme-aware, responsive skeleton component."
},
{
"name": "Spinner",
"category": "base",
"purpose": "Theme-aware, responsive spinner component."
},
{
"name": "StyledIcon",
"category": "base",
"purpose": "Theme-aware, responsive styled icon component."
},
{
"name": "Toast",
"category": "base",
"purpose": "Theme-aware, responsive toast component."
},
{
"name": "Timeline",
"category": "base",
"purpose": "Theme-aware, responsive timeline component."
},
{
"name": "TreeView",
"category": "base",
"purpose": "Theme-aware, responsive tree view component."
},
{
"name": "Marquee",
"category": "base",
"purpose": "Theme-aware, responsive marquee component."
},
{
"name": "Navbar",
"category": "navigation",
"purpose": "Theme-aware, responsive navbar component."
},
{
"name": "Footer",
"category": "navigation",
"purpose": "Responsive application footer with structured links and pre/post content slots."
},
{
"name": "MegaMenu",
"category": "navigation",
"purpose": "Theme-aware, responsive mega menu component."
},
{
"name": "Nav",
"category": "navigation",
"purpose": "Theme-aware, responsive nav component."
},
{
"name": "Tabs",
"category": "navigation",
"purpose": "Theme-aware, responsive tabs component."
},
{
"name": "Sidebar",
"category": "navigation",
"purpose": "Theme-aware, responsive sidebar component."
},
{
"name": "Scrollspy",
"category": "navigation",
"purpose": "Theme-aware, responsive scrollspy component."
},
{
"name": "Breadcrumb",
"category": "navigation",
"purpose": "Theme-aware, responsive breadcrumb component."
},
{
"name": "Pagination",
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component."
},
{
"name": "Stepper",
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component."
},
{
"name": "Input",
"category": "forms",
"purpose": "Theme-aware, responsive input component."
},
{
"name": "InputGroup",
"category": "forms",
"purpose": "Theme-aware, responsive input group component."
},
{
"name": "Textarea",
"category": "forms",
"purpose": "Theme-aware, responsive textarea component."
},
{
"name": "FileInput",
"category": "forms",
"purpose": "Theme-aware, responsive file input component."
"purpose": "Theme-aware, responsive chat bubble component."
},
{
"name": "Checkbox",
@@ -262,24 +108,14 @@
"purpose": "Theme-aware, responsive checkbox component."
},
{
"name": "Radio",
"category": "forms",
"purpose": "Theme-aware, responsive radio component."
"name": "Clipboard",
"category": "integrations",
"purpose": "Theme-aware, responsive clipboard component."
},
{
"name": "Switch",
"category": "forms",
"purpose": "Theme-aware, responsive switch component."
},
{
"name": "Select",
"category": "forms",
"purpose": "Theme-aware, responsive select component."
},
{
"name": "RangeSlider",
"category": "forms",
"purpose": "Theme-aware, responsive range slider component."
"name": "Collapse",
"category": "base",
"purpose": "Theme-aware, responsive collapse component."
},
{
"name": "ColorPicker",
@@ -287,15 +123,9 @@
"purpose": "Theme-aware, responsive color picker component."
},
{
"name": "TimePicker",
"category": "forms",
"purpose": "Theme-aware, responsive time picker component."
},
{
"name": "AdvancedSelect",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive advanced select component.",
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
"name": "Columns",
"category": "layout",
"purpose": "Theme-aware, responsive columns component."
},
{
"name": "ComboBox",
@@ -304,46 +134,14 @@
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
},
{
"name": "SearchBox",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive search box component."
"name": "Confetti",
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component."
},
{
"name": "InputNumber",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive input number component."
},
{
"name": "StrongPassword",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive strong password component."
},
{
"name": "TogglePassword",
"category": "advanced-forms",
"purpose": "Accessible password field with optional show and hide controls.",
"events": ["input", "change", "toggle"]
},
{
"name": "ToggleCount",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive toggle count component."
},
{
"name": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component."
},
{
"name": "PinInput",
"category": "advanced-forms",
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support.",
"events": ["input", "change", "complete", "paste", "clear", "error"]
},
{
"name": "Dropdown",
"category": "overlays",
"purpose": "Theme-aware, responsive dropdown component."
"name": "Container",
"category": "layout",
"purpose": "Theme-aware, responsive container component."
},
{
"name": "ContextMenu",
@@ -351,54 +149,19 @@
"purpose": "Theme-aware, responsive context menu component."
},
{
"name": "Modal",
"category": "overlays",
"purpose": "Theme-aware, responsive modal component."
"name": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component."
},
{
"name": "Drawer",
"category": "overlays",
"purpose": "Theme-aware, responsive drawer component."
"name": "CTASection",
"category": "blocks",
"purpose": "Reusable closing call-to-action section with primary and secondary actions."
},
{
"name": "Popover",
"category": "overlays",
"purpose": "Theme-aware, responsive popover component."
},
{
"name": "Tooltip",
"category": "overlays",
"purpose": "Theme-aware, responsive tooltip component."
},
{
"name": "Table",
"category": "tables",
"purpose": "Theme-aware, responsive table component."
},
{
"name": "AdvancedRangeSlider",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced range slider component."
},
{
"name": "AdvancedDatePicker",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced date picker component."
},
{
"name": "Chart",
"category": "integrations",
"purpose": "Theme-aware, responsive chart component."
},
{
"name": "Clipboard",
"category": "integrations",
"purpose": "Theme-aware, responsive clipboard component."
},
{
"name": "Confetti",
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component."
"name": "CustomScrollbar",
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component."
},
{
"name": "DataMap",
@@ -410,35 +173,201 @@
"category": "integrations",
"purpose": "Theme-aware, responsive data table component."
},
{
"name": "DatePicker",
"category": "base",
"purpose": "Theme-aware, responsive date picker component."
},
{
"name": "DeviceFrame",
"category": "base",
"purpose": "Theme-aware, responsive device frame component."
},
{
"name": "Divider",
"category": "layout",
"purpose": "Theme-aware, responsive divider component."
},
{
"name": "DragAndDrop",
"category": "integrations",
"purpose": "Theme-aware, responsive drag and drop component."
},
{
"name": "Drawer",
"category": "overlays",
"purpose": "Theme-aware, responsive drawer component."
},
{
"name": "Dropdown",
"category": "overlays",
"purpose": "Theme-aware, responsive dropdown component."
},
{
"name": "FeatureCard",
"category": "blocks",
"purpose": "Reusable linked feature or service card with icon, badge, description, and action."
},
{
"name": "FeatureGrid",
"category": "layout",
"purpose": "Responsive equal-height grid for feature and service cards."
},
{
"name": "FeatureIconCard",
"category": "blocks",
"purpose": "Feature card with a prominent themed icon treatment."
},
{
"name": "FileInput",
"category": "forms",
"purpose": "Theme-aware, responsive file input component."
},
{
"name": "FileUpload",
"category": "integrations",
"purpose": "Theme-aware, responsive file upload component."
},
{
"name": "FileUploadProgress",
"category": "base",
"purpose": "Theme-aware, responsive file upload progress component."
},
{
"name": "Footer",
"category": "navigation",
"purpose": "Responsive application footer with structured links and pre/post content slots."
},
{
"name": "Grid",
"category": "layout",
"purpose": "Theme-aware, responsive grid component."
},
{
"name": "Hero",
"category": "blocks",
"purpose": "Reusable public-page hero with actions, trust items, badges, and visual slots."
},
{
"name": "HeroActions",
"category": "base",
"purpose": "Responsive action group for hero and call-to-action sections."
},
{
"name": "Image",
"category": "layout",
"purpose": "Theme-aware, responsive image component."
},
{
"name": "Input",
"category": "forms",
"purpose": "Theme-aware, responsive input component."
},
{
"name": "InputGroup",
"category": "forms",
"purpose": "Theme-aware, responsive input group component."
},
{
"name": "InputNumber",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive input number component."
},
{
"name": "Kbd",
"category": "layout",
"purpose": "Theme-aware, responsive kbd component."
},
{
"name": "LayoutSplitter",
"category": "layout",
"purpose": "Theme-aware, responsive layout splitter component."
},
{
"name": "LegendIndicator",
"category": "base",
"purpose": "Theme-aware, responsive legend indicator component."
},
{
"name": "Link",
"category": "layout",
"purpose": "Theme-aware, responsive link component."
},
{
"name": "List",
"category": "base",
"purpose": "Theme-aware, responsive list component."
},
{
"name": "ListGroup",
"category": "base",
"purpose": "Theme-aware, responsive list group component."
},
{
"name": "Map",
"category": "integrations",
"purpose": "Theme-aware, responsive map component."
},
{
"name": "ToastNotifications",
"category": "integrations",
"purpose": "Theme-aware, responsive toast notifications component."
"name": "MarketingSectionHeader",
"category": "blocks",
"purpose": "Marketing section heading with an optional linked action."
},
{
"name": "WysiwygEditor",
"category": "integrations",
"purpose": "Theme-aware, responsive wysiwyg editor component."
"name": "Marquee",
"category": "base",
"purpose": "Theme-aware, responsive marquee component."
},
{
"name": "PreferenceSwitcher",
"name": "MegaMenu",
"category": "navigation",
"purpose": "Accessible theme, accent color, and language preference controls."
"purpose": "Theme-aware, responsive mega menu component."
},
{
"name": "MetricCard",
"category": "blocks",
"purpose": "Single statistic or operational metric with icon, trend, and optional action."
},
{
"name": "MetricGrid",
"category": "layout",
"purpose": "Responsive collection of metric cards."
},
{
"name": "Modal",
"category": "overlays",
"purpose": "Theme-aware, responsive modal component."
},
{
"name": "Nav",
"category": "navigation",
"purpose": "Theme-aware, responsive nav component."
},
{
"name": "Navbar",
"category": "navigation",
"purpose": "Theme-aware, responsive navbar component."
},
{
"name": "PageHeader",
"category": "blocks",
"purpose": "Public and application page header with breadcrumbs, metadata, and actions."
},
{
"name": "Pagination",
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component."
},
{
"name": "PinInput",
"category": "advanced-forms",
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support.",
"events": ["input", "change", "complete", "paste", "clear", "error"]
},
{
"name": "Popover",
"category": "overlays",
"purpose": "Theme-aware, responsive popover component."
},
{
"name": "PortalDashboard",
@@ -446,14 +375,175 @@
"purpose": "Responsive portal dashboard with metrics, actions, tasks, and updates."
},
{
"name": "AuthSplitLayout",
"category": "layout",
"purpose": "Responsive 50/50 authentication layout with content and form regions."
"name": "PreferenceSwitcher",
"category": "navigation",
"purpose": "Accessible theme, accent color, and language preference controls."
},
{
"name": "AuthForm",
"name": "Progress",
"category": "base",
"purpose": "Theme-aware, responsive progress component."
},
{
"name": "PublicPageShell",
"category": "layout",
"purpose": "Consistent structural wrapper for public-facing pages."
},
{
"name": "Radio",
"category": "forms",
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA."
"purpose": "Theme-aware, responsive radio component."
},
{
"name": "RangeSlider",
"category": "forms",
"purpose": "Theme-aware, responsive range slider component."
},
{
"name": "Rating",
"category": "base",
"purpose": "Theme-aware, responsive rating component."
},
{
"name": "Scrollspy",
"category": "navigation",
"purpose": "Theme-aware, responsive scrollspy component."
},
{
"name": "SearchBox",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive search box component."
},
{
"name": "Section",
"category": "layout",
"purpose": "Theme-aware page section with consistent spacing, width, background, and border controls."
},
{
"name": "SectionHeader",
"category": "layout",
"purpose": "Reusable accessible section heading with eyebrow, title, description, icon, and actions."
},
{
"name": "Select",
"category": "forms",
"purpose": "Theme-aware, responsive select component."
},
{
"name": "Sidebar",
"category": "navigation",
"purpose": "Theme-aware, responsive sidebar component."
},
{
"name": "Skeleton",
"category": "base",
"purpose": "Theme-aware, responsive skeleton component."
},
{
"name": "Spinner",
"category": "base",
"purpose": "Theme-aware, responsive spinner component."
},
{
"name": "SplitHero",
"category": "blocks",
"purpose": "Two-column hero with configurable content and visual placement."
},
{
"name": "StatsBar",
"category": "blocks",
"purpose": "Compact responsive statistics strip."
},
{
"name": "Stepper",
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component."
},
{
"name": "StrongPassword",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive strong password component."
},
{
"name": "StyledIcon",
"category": "base",
"purpose": "Theme-aware, responsive styled icon component."
},
{
"name": "Switch",
"category": "forms",
"purpose": "Theme-aware, responsive switch component."
},
{
"name": "Table",
"category": "tables",
"purpose": "Theme-aware, responsive table component."
},
{
"name": "Tabs",
"category": "navigation",
"purpose": "Theme-aware, responsive tabs component."
},
{
"name": "Textarea",
"category": "forms",
"purpose": "Theme-aware, responsive textarea component."
},
{
"name": "TextLink",
"category": "layout",
"purpose": "Styled inline action link with icons, arrows, external state, and disabled handling."
},
{
"name": "Timeline",
"category": "base",
"purpose": "Theme-aware, responsive timeline component."
},
{
"name": "TimePicker",
"category": "forms",
"purpose": "Theme-aware, responsive time picker component."
},
{
"name": "Toast",
"category": "base",
"purpose": "Theme-aware, responsive toast component."
},
{
"name": "ToastNotifications",
"category": "integrations",
"purpose": "Theme-aware, responsive toast notifications component."
},
{
"name": "ToggleCount",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive toggle count component."
},
{
"name": "TogglePassword",
"category": "advanced-forms",
"purpose": "Accessible password field with optional show and hide controls.",
"events": ["input", "change", "toggle"]
},
{
"name": "Tooltip",
"category": "overlays",
"purpose": "Theme-aware, responsive tooltip component."
},
{
"name": "TreeView",
"category": "base",
"purpose": "Theme-aware, responsive tree view component."
},
{
"name": "Typography",
"category": "layout",
"purpose": "Theme-aware, responsive typography component."
},
{
"name": "WysiwygEditor",
"category": "integrations",
"purpose": "Theme-aware, responsive wysiwyg editor component."
}
]
}
File diff suppressed because it is too large Load Diff
+23 -1
View File
@@ -7,7 +7,29 @@ component Columns {
maxWidth = "xl"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--columns wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
data-ui-component="Columns"
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"'
>
<slot></slot>
</div>
}
}
+31 -5
View File
@@ -1,7 +1,5 @@
component Image {
props {
@event load = function
@event error = function
size = "default"
color = "primary"
src = ""
@@ -12,10 +10,38 @@ component Image {
rounded = false
class = ""
}
view {
<figure class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--image {rounded ? 'wire-next--rounded' : ''} {class}">
<img src="{src}" alt="{alt}" width="{width}" height="{height}" loading="{loading}" />
<slot />
<figure
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='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}
<slot></slot>
</figure>
}
}
+33 -4
View File
@@ -1,8 +1,5 @@
component Link {
props {
@event click = function
@event focus = function
@event blur = function
size = "default"
color = "primary"
label = "Link"
@@ -12,7 +9,39 @@ component Link {
external = false
class = ""
}
view {
<a {...attrs} href="{href}" target="{target}" rel="{rel}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--link {class}"><slot>{label}</slot>{#if external}<span aria-hidden="true">↗</span>{/if}</a>
<a
data-ui-component="Link"
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'
>
<span class="truncate">{label}</span>
{#if external}
<span
class="icon-[lucide--external-link] size-3.5 shrink-0"
aria-hidden="true"
></span>
{/if}
<slot></slot>
</a>
}
}
+93 -7
View File
@@ -1,7 +1,5 @@
component List {
props {
@event select = function
@event click = function
size = "default"
color = "primary"
title = "List"
@@ -10,12 +8,100 @@ component List {
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--list wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="List"
aria-label='{title}'
class='w-full {class}'
>
{#if title || description}
<header class="mb-4">
{#if title}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
{/if}
{#if description}
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
</header>
{/if}
<ul
class="overflow-hidden"
class:divide-y='variant === "default" || variant === "divided"'
class:divide-[var(--wire-color-border)]='variant === "default" || variant === "divided"'
class:rounded-2xl='variant === "card"'
class:border='variant === "card"'
class:border-[var(--wire-color-border)]='variant === "card"'
class:bg-[var(--wire-color-surface-raised)]='variant === "card"'
class:space-y-3='variant === "separated"'
>
{#each items as item, index}
<li
class="group min-w-0"
class:rounded-xl='variant === "separated"'
class:border='variant === "separated"'
class:border-[var(--wire-color-border)]='variant === "separated"'
class:bg-[var(--wire-color-surface-raised)]='variant === "separated"'
>
{#if item.href}
<a
href='{item.href}'
class="flex min-w-0 items-start gap-3 px-4 py-3 outline-none transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
class:px-3='size === "sm"'
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 } }))'
>
{#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)]">
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
{#if item.imageSrc}
<img src='{item.imageSrc}' alt='{item.imageAlt || ""}' class="size-12 shrink-0 rounded-xl object-cover" loading="lazy" />
{/if}
<div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-3">
<p class="truncate font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
{#if item.meta}
<span class="shrink-0 text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
{/if}
</div>
{#if item.description}
<p class="mt-1 line-clamp-2 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
{#if item.badge}
<span class="mt-2 inline-flex rounded-full bg-[var(--wire-color-primary-soft)] px-2.5 py-1 text-xs font-semibold text-[var(--wire-color-primary)]">{item.badge}</span>
{/if}
</div>
<span class="icon-[lucide--chevron-right] mt-1 size-4 shrink-0 text-[var(--wire-color-text-muted)] transition group-hover:translate-x-0.5 group-hover:text-[var(--wire-color-primary)]" aria-hidden="true"></span>
</a>
{:else}
<div class="flex min-w-0 items-start gap-3 px-4 py-3">
{#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)]">
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
<div class="min-w-0 flex-1">
<p class="font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
{#if item.description}
<p class="mt-1 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
</div>
</div>
{/if}
</li>
{:empty}
<li class="rounded-xl border border-dashed border-[var(--wire-color-border)] p-6 text-center text-sm text-[var(--wire-color-text-muted)]">
No items available.
</li>
{/each}
</ul>
<slot></slot>
</section>
}
}
+71 -9
View File
@@ -1,9 +1,5 @@
component Map {
props {
@event ready = function
@event move = function
@event zoom = function
@event markerClick = function
size = "default"
color = "primary"
title = "Map"
@@ -12,12 +8,78 @@ component Map {
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--map wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="Map"
aria-label='{title}'
class='overflow-hidden rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] shadow-sm {class}'
>
{#if title || description}
<header class="flex items-start justify-between gap-4 border-b border-[var(--wire-color-border)] p-5">
<div>
{#if title}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
{/if}
{#if description}
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
</div>
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
<span class="icon-[lucide--map] size-5" aria-hidden="true"></span>
</span>
</header>
{/if}
<div
class="relative isolate overflow-hidden bg-[var(--wire-color-surface-soft)]"
class:h-64='size === "sm"'
class:h-80='size === "default" || size === "md"'
class:h-[28rem]='size === "lg"'
>
<div
class="pointer-events-none absolute inset-0 opacity-50"
style="background-image: linear-gradient(var(--wire-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--wire-color-border) 1px, transparent 1px); background-size: 32px 32px;"
aria-hidden="true"
></div>
<div class="absolute inset-0 flex items-center justify-center p-6">
<slot></slot>
</div>
{#if items.length > 0}
{#each items as item, index}
<button
type="button"
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 }))'
>
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
</button>
{/each}
{/if}
<div class="absolute bottom-4 right-4 flex flex-col gap-2">
<button
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" } }))'
>
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
</button>
<button
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" } }))'
>
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
</button>
</div>
</div>
</section>
}
}
+97 -7
View File
@@ -1,7 +1,5 @@
component Marquee {
props {
@event pause = function
@event resume = function
size = "default"
color = "primary"
title = "Marquee"
@@ -10,12 +8,104 @@ component Marquee {
variant = "default"
class = ""
}
state paused = false
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--marquee wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="Marquee"
aria-label='{title}'
class='overflow-hidden border-y border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] {class}'
>
<div class="flex items-stretch">
{#if title}
<div class="relative z-10 flex shrink-0 items-center gap-2 border-r border-[var(--wire-color-border)] bg-[var(--wire-color-primary)] px-4 py-3 text-[var(--wire-color-on-primary)] sm:px-5">
<span class="icon-[lucide--megaphone] size-4" aria-hidden="true"></span>
<span class="text-sm font-bold">{title}</span>
</div>
{/if}
<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 }))'
@focusin='paused = true'
@focusout='paused = false'
>
<div
class="wire-marquee-track flex w-max min-w-full items-center"
class:wire-marquee-paused='paused'
>
{#each items as item}
<a
href='{item.href || "#"}'
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)] transition hover:bg-[var(--wire-color-primary-soft)] hover:text-[var(--wire-color-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
>
<span
class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]"
class:bg-[var(--wire-color-danger)]='item.color === "danger"'
class:bg-[var(--wire-color-warning)]='item.color === "warning"'
class:bg-[var(--wire-color-success)]='item.color === "success"'
aria-hidden="true"
></span>
<span>{item.label || item.title}</span>
{#if item.meta}
<span class="text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
{/if}
</a>
{:empty}
<p class="px-5 py-3 text-sm text-[var(--wire-color-text-muted)]">{description || "No announcements available."}</p>
{/each}
{#if items.length > 0}
{#each items as item}
<a
href='{item.href || "#"}'
aria-hidden="true"
tabindex="-1"
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)]"
>
<span class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]" aria-hidden="true"></span>
<span>{item.label || item.title}</span>
</a>
{/each}
{/if}
</div>
</div>
<button
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 }))'
>
<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>
</button>
</div>
<slot></slot>
</section>
}
style {
.wire-marquee-track {
animation: wire-marquee 32s linear infinite;
}
.wire-marquee-paused {
animation-play-state: paused;
}
@keyframes wire-marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.wire-marquee-track {
animation: none;
}
}
}
}
+61 -6
View File
@@ -1,10 +1,5 @@
component SearchBox {
props {
@event input = function
@event change = function
@event search = function
@event submit = function
@event clear = function
size = "default"
color = "primary"
label = "Search Box"
@@ -19,7 +14,67 @@ component SearchBox {
required = false
class = ""
}
state query = value
view {
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--search-box {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
<form
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 } }))'
>
<label
class="mb-2 block text-sm font-semibold text-[var(--wire-color-text)]"
class:sr-only='label === ""'
>
{label}
</label>
<div class="relative flex items-center">
<span
class="icon-[lucide--search] pointer-events-none absolute left-4 size-5 text-[var(--wire-color-input-placeholder)]"
aria-hidden="true"
></span>
<input
name='{name}'
type='{type}'
value='{query}'
placeholder='{placeholder}'
min='{min}'
max='{max}'
step='{step}'
disabled='{disabled}'
required='{required}'
autocomplete="off"
class="w-full rounded-xl border border-[var(--wire-color-input-border)] bg-[var(--wire-color-input-background)] pl-12 pr-24 text-[var(--wire-color-input-text)] outline-none transition placeholder:text-[var(--wire-color-input-placeholder)] hover:border-[var(--wire-color-input-border-hover)] focus:border-[var(--wire-color-input-border-focus)] focus:ring-2 focus:ring-[var(--wire-color-focus)] disabled:cursor-not-allowed disabled:opacity-60"
class:h-10='size === "sm"'
class:h-12='size === "default" || size === "md"'
class:h-14='size === "lg"'
@input='query = event.target.value'
@change='query = event.target.value'
/>
<button
type="button"
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 } }))'
>
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
</button>
<button
type="submit"
aria-label='{label || "Search"}'
disabled='{disabled}'
class="absolute right-2 inline-flex size-9 items-center justify-center rounded-lg bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] transition hover:bg-[var(--wire-color-primary-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] disabled:cursor-not-allowed disabled:opacity-60"
>
<span class="icon-[lucide--arrow-right] size-4" aria-hidden="true"></span>
</button>
</div>
</form>
}
}
+77 -6
View File
@@ -1,7 +1,5 @@
component Tabs {
props {
@event change = function
@event select = function
size = "default"
color = "primary"
label = "Tabs"
@@ -10,10 +8,83 @@ component Tabs {
orientation = "horizontal"
class = ""
}
state activeValue = active || (items[0] ? (items[0].value || items[0].id || "0") : "")
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--tabs 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>
<section
data-ui-component="Tabs"
class='w-full {class}'
class:flex='orientation === "vertical"'
class:items-start='orientation === "vertical"'
class:gap-6='orientation === "vertical"'
>
<div
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"'
>
{#each items as item, index}
<button
type="button"
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 } }))'
>
{#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}
</button>
{/each}
</div>
<div class="min-w-0 flex-1 pt-4" class:pt-0='orientation === "vertical"'>
{#each items as item, index}
<div
role="tabpanel"
id='panel-{item.value || item.id || index}'
aria-labelledby='tab-{item.value || item.id || 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)]"
>
{#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}
</div>
{/each}
<slot></slot>
</div>
</section>
}
}
+69 -6
View File
@@ -1,6 +1,5 @@
component Timeline {
props {
@event select = function
size = "default"
color = "primary"
title = "Timeline"
@@ -9,12 +8,76 @@ component Timeline {
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--timeline wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="Timeline"
aria-label='{title}'
class='w-full {class}'
>
{#if title || description}
<header class="mb-6">
{#if title}
<h3 class="text-xl font-bold text-[var(--wire-color-text)]">{title}</h3>
{/if}
{#if description}
<p class="mt-2 max-w-2xl leading-7 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
</header>
{/if}
<ol class="relative ml-4 border-l border-[var(--wire-color-border)]">
{#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 } }))'
>
<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"
class:bg-[var(--wire-color-success)]='item.status === "complete" || item.status === "success"'
class:bg-[var(--wire-color-warning)]='item.status === "warning" || item.status === "pending"'
class:bg-[var(--wire-color-danger)]='item.status === "danger" || item.status === "failed"'
>
<span class='{item.icon || "icon-[lucide--circle]"}' aria-hidden="true"></span>
</span>
<article
class="rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] p-5 shadow-sm"
class:border-transparent='variant === "minimal"'
class:bg-transparent='variant === "minimal"'
class:p-0='variant === "minimal"'
class:shadow-none='variant === "minimal"'
>
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<h4 class="font-bold text-[var(--wire-color-text)]">{item.title || item.label}</h4>
{#if item.subtitle}
<p class="mt-1 text-sm font-medium text-[var(--wire-color-primary)]">{item.subtitle}</p>
{/if}
</div>
{#if item.date || item.meta}
<time class="shrink-0 text-sm text-[var(--wire-color-text-muted)]">{item.date || item.meta}</time>
{/if}
</div>
{#if item.description}
<p class="mt-3 text-sm leading-6 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
{#if item.href}
<a href='{item.href}' class="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-[var(--wire-color-primary)] hover:text-[var(--wire-color-primary-hover)]">
<span>{item.actionLabel || "View details"}</span>
<span class="icon-[lucide--arrow-right] size-4" aria-hidden="true"></span>
</a>
{/if}
</article>
</li>
{:empty}
<li class="pl-8 text-sm text-[var(--wire-color-text-muted)]">No timeline items available.</li>
{/each}
</ol>
<slot></slot>
</section>
}
}
+63 -1
View File
@@ -7,7 +7,69 @@ component Typography {
maxWidth = "xl"
class = ""
}
view {
<article class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--typography wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></article>
<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"'
>
<slot></slot>
</div>
}
style {
.wire-typography :where(h1, h2, h3, h4) {
color: var(--wire-color-text);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.2;
break-after: avoid;
}
.wire-typography :where(h1) { font-size: clamp(2rem, 4vw, 3.5rem); margin: 0 0 1.5rem; }
.wire-typography :where(h2) { font-size: clamp(1.5rem, 3vw, 2.25rem); margin: 2.5rem 0 1rem; }
.wire-typography :where(h3) { font-size: 1.35rem; margin: 2rem 0 0.75rem; }
.wire-typography :where(p, ul, ol, blockquote, pre, table) { margin: 1rem 0; }
.wire-typography :where(p, li) { color: var(--wire-color-text-muted); line-height: 1.8; }
.wire-typography :where(a) { color: var(--wire-color-primary); font-weight: 600; text-underline-offset: 0.2em; }
.wire-typography :where(a:hover) { color: var(--wire-color-primary-hover); text-decoration: underline; }
.wire-typography :where(ul, ol) { padding-left: 1.4rem; }
.wire-typography :where(ul) { list-style: disc; }
.wire-typography :where(ol) { list-style: decimal; }
.wire-typography :where(blockquote) {
border-left: 4px solid var(--wire-color-primary);
background: var(--wire-color-primary-soft);
border-radius: 0 0.75rem 0.75rem 0;
padding: 1rem 1.25rem;
color: var(--wire-color-text);
}
.wire-typography :where(code) {
border-radius: 0.35rem;
background: var(--wire-color-surface-soft);
padding: 0.15rem 0.35rem;
font-size: 0.9em;
}
.wire-typography :where(pre) {
overflow-x: auto;
border: 1px solid var(--wire-color-border);
border-radius: 1rem;
background: var(--wire-color-surface-raised);
padding: 1rem;
}
.wire-typography :where(img) { border-radius: 1rem; }
.wire-typography :where(hr) { border-color: var(--wire-color-border); margin: 2rem 0; }
}
}
+222 -132
View File
@@ -39,221 +39,311 @@ component Card {
class = ""
}
state visible = true
state selectedNav = activeNav
state dismissed = false
functions {
function chooseNavigation(item, index) {
previousValue = selectedNav
selectedNav = item.value || item.label
$emit("navigate", {
value: selectedNav,
previousValue: previousValue,
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)
}
function chooseMobileNavigation(sourceEvent) {
previousValue = selectedNav
selectedNav = sourceEvent.target.value
$emit("navigate", {
value: selectedNav,
previousValue: previousValue
function dispatchCardNavigationValue(sourceEvent, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("navigate", true, false, {
component: "Card",
value: sourceEvent.currentTarget.value
})
root.dispatchEvent(customEvent)
}
function chooseAction(action, index) {
$emit("action", {
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)
}
function dismissCard(sourceEvent) {
panel = sourceEvent.currentTarget.closest(".wire-next__card-panel")
if (panel) {
panel.setAttribute("hidden", "")
function dismissCard(sourceEvent, root, customEvent) {
dismissed = true
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
visible = false
$emit("dismiss", {
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("dismiss", true, false, {
component: "Card",
title: title
})
root.dispatchEvent(customEvent)
}
}
view {
<section
<div
{...attrs}
class="wire-next wire-next--card {class}"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-layout="{layout}"
data-align="{align}"
data-hover="{hover}"
data-image-position="{imagePosition}"
data-group="{items.length > 0}"
aria-label="{ariaLabel || title}"
data-ui-component="Card"
data-wrn-card
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
class='wire-next wire-next--card wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}'
>
{#if items.length > 0}
<div class="wire-next__card-group">
{#each items as item}
<article class="wire-next__card-panel">
{#if item.imageSrc}
{#each items as item, itemIndex}
<article
class="wire-next__card-panel"
aria-label='{item.ariaLabel || item.title || item.label || "Card item"}'
data-index='{itemIndex}'
>
{#if item.imageSrc || item.image}
<div class="wire-next__card-media">
<img src="{item.imageSrc}" alt="{item.imageAlt || ''}" loading="lazy" />
<img
src='{item.imageSrc || item.image}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-body">
{#if item.title}<h3>{item.title}</h3>{/if}
{#if item.description}<p>{item.description}</p>{/if}
<div class="wire-next__card-content">
{#if item.title || item.label}
<h3>{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wire-next__card-subtitle">{item.subtitle}</p>
{/if}
{#if item.description}
<p class="wire-next__card-description">{item.description}</p>
{/if}
{#if item.actionLabel || item.href}
<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 } }))'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</div>
{#if item.footer}<footer class="wire-next__card-footer">{item.footer}</footer>{/if}
</article>
{/each}
</div>
{:else if visible}
<article class="wire-next__card-panel">
{#if imagePosition === "overlay" && imageSrc}
<div class="wire-next__card-media wire-next__card-media--overlay">
{:else}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if imageSrc && imagePosition === "overlay"}
<div class="wire-next__card-overlay">
<img
src="{imageSrc}"
alt="{imageAlt}"
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
@load="$emit('load', { src: imageSrc })"
@error="$emit('error', { src: imageSrc })"
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 } }))'
/>
<div class="wire-next__card-overlay">
{#if title}<h3>{title}</h3>{/if}
{#if subtitle}<span class="wire-next__card-subtitle">{subtitle}</span>{/if}
{#if description}<p>{description}</p>{/if}
{#if footer}<small>{footer}</small>{/if}
</div>
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
</div>
{:else}
{#if header || headerActions.length > 0 || dismissible}
{/if}
{#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if header || title || subtitle || headerActions.length > 0 || dismissible}
<header class="wire-next__card-header">
<span>{header}</span>
<div class="wire-next__card-heading">
{#if header}
<p class="wire-next__card-eyebrow">{header}</p>
{/if}
{#if title}
<h3>{title}</h3>
{/if}
{#if subtitle}
<p class="wire-next__card-subtitle">{subtitle}</p>
{/if}
</div>
{#if headerActions.length > 0 || dismissible}
<span class="wire-next__card-header-actions">
{#each headerActions as action, index}
<div class="wire-next__card-header-actions">
{#each headerActions as headerAction, actionIndex}
<button
type="button"
aria-label="{action.ariaLabel || action.label}"
title="{action.label}"
@click="chooseAction(action, index)"
aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
@click='dispatchCardHeaderAction(event, headerAction, actionIndex)'
>
{#if action.icon}<span class="{action.icon}" aria-hidden="true"></span>{/if}
{#if !action.icon}<span>{action.label}</span>{/if}
{#if headerAction.icon}
<span class='{headerAction.icon}' aria-hidden="true"></span>
{/if}
{#if headerAction.label && !headerAction.icon}
<span>{headerAction.label}</span>
{/if}
</button>
{/each}
{#if dismissible}
<button type="button" aria-label="Close card" @click="dismissCard(event)">
<button
type="button"
aria-label="Dismiss card"
@click='dismissCard(event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</span>
</div>
{/if}
</header>
{/if}
{#if navigation.length > 0}
<nav class="wire-next__card-navigation" aria-label="{ariaLabel || title}">
<div class="wire-next__card-tabs">
{#each navigation as item, index}
<button
type="button"
data-active="{selectedNav === (item.value || item.label)}"
aria-current="{selectedNav === (item.value || item.label) ? 'page' : ''}"
@click="chooseNavigation(item, index)"
>
{item.label}
</button>
{/each}
</div>
<nav aria-label='{ariaLabel || title || "Card navigation"}' class="wire-next__card-navigation">
{#if mobileNavigation}
<select
class="wire-next__card-navigation-select"
aria-label="{ariaLabel || title}"
value="{selectedNav}"
@change="chooseMobileNavigation(event)"
aria-label='{ariaLabel || title || "Card navigation"}'
@change='dispatchCardNavigationValue(event)'
>
{#each navigation as item}
{#each navigation as navItem}
<option
value="{item.value || item.label}"
selected="{selectedNav === (item.value || item.label)}"
value='{navItem.value || navItem.href || navItem.label}'
selected='{activeNav === navItem.value || activeNav === navItem.href}'
>
{item.label}
{navItem.label}
</option>
{/each}
</select>
{/if}
<div class="wire-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
{#each navigation as navItem, navIndex}
<button
type="button"
aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
@click='dispatchCardNavigation(event, navItem, navIndex)'
>
{#if navItem.icon}
<span class='{navItem.icon}' aria-hidden="true"></span>
{/if}
<span>{navItem.label}</span>
{#if navItem.badge}
<span class="wire-next__card-tab-badge">{navItem.badge}</span>
{/if}
</button>
{/each}
</div>
</nav>
{/if}
{#if imageSrc && imagePosition !== "bottom"}
<div class="wire-next__card-media">
<img
src="{imageSrc}"
alt="{imageAlt}"
loading="lazy"
@load="$emit('load', { src: imageSrc })"
@error="$emit('error', { src: imageSrc })"
/>
</div>
{/if}
{#if alertTitle || alertDescription}
<aside class="wire-next__card-alert" role="status">
{#if alertTitle}<strong>{alertTitle}</strong>{/if}
{#if alertDescription}<span>{alertDescription}</span>{/if}
</aside>
<div class="wire-next__card-alert" role="status">
{#if alertTitle}
<strong>{alertTitle}</strong>
{/if}
{#if alertDescription}
<p>{alertDescription}</p>
{/if}
</div>
{/if}
<div
class="wire-next__card-body"
data-scrollable="{scrollable}"
style="--wire-card-max-height: {maxHeight}"
data-scrollable='{scrollable}'
style='max-height: {scrollable ? maxHeight : "none"};'
>
{#if empty}
<div class="wire-next__card-empty">
<span class="{emptyIcon}" aria-hidden="true"></span>
<strong>{emptyTitle}</strong>
<span class='{emptyIcon}' aria-hidden="true"></span>
<p>{emptyTitle}</p>
</div>
{:else}
{#if title}<h3>{title}</h3>{/if}
{#if subtitle}<span class="wire-next__card-subtitle">{subtitle}</span>{/if}
{#if description}<p>{description}</p>{/if}
<slot />
{#if actionLabel}
<a
class="wire-next__card-action"
href="{actionHref || '#'}"
@click="$emit('action', { label: actionLabel, href: actionHref })"
>
{actionLabel}<span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
</a>
{#if description}
<p class="wire-next__card-description">{description}</p>
{/if}
<slot></slot>
{/if}
</div>
{#if footer}<footer class="wire-next__card-footer">{footer}</footer>{/if}
{#if imageSrc && imagePosition === "bottom"}
<div class="wire-next__card-media">
<img
src="{imageSrc}"
alt="{imageAlt}"
loading="lazy"
@load="$emit('load', { src: imageSrc })"
@error="$emit('error', { src: imageSrc })"
/>
</div>
{#if footer || actionLabel}
<footer class="wire-next__card-footer">
{#if footer}
<span>{footer}</span>
{/if}
{#if actionLabel}
<a
href='{actionHref || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</footer>
{/if}
</div>
{#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
</article>
{/if}
</section>
</div>
}
}
+34 -1
View File
@@ -7,7 +7,40 @@ component Container {
maxWidth = "xl"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--container wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
data-ui-component="Container"
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"'
>
<slot></slot>
</div>
}
}
+58 -1
View File
@@ -6,7 +6,64 @@ component Divider {
orientation = "horizontal"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--divider wire-next--divider-{orientation} {class}" role="separator" aria-orientation="{orientation}">{#if label}<span>{label}</span>{/if}</div>
<div
{...attrs}
data-ui-component="Divider"
data-size='{size}'
data-color='{color}'
data-orientation='{orientation}'
class='w-full {class}'
class:h-full='orientation === "vertical"'
class:w-auto='orientation === "vertical"'
>
{#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}
</div>
}
}
+28 -1
View File
@@ -7,7 +7,34 @@ component Grid {
maxWidth = "xl"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--grid wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
data-ui-component="Grid"
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"'
>
<slot></slot>
</div>
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT",
@@ -26,7 +26,7 @@
"./ui.css": "./ui.css"
},
"dependencies": {
"@wrnexus/core": "^0.5.11"
"@wrnexus/core": "^0.5.12"
},
"files": [
"dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/uploader — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.5.11"
"@wrnexus/core": "^0.5.12"
},
"files": [
"dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.",
"license": "MIT",
@@ -0,0 +1,284 @@
component Card {
props {
@event click = function
@event action = function
@event navigate = function
@event dismiss = function
@event load = function
@event error = function
title = "Card title"
subtitle = ""
description = ""
header = ""
footer = ""
imageSrc = ""
imageAlt = ""
imagePosition = "top"
actionLabel = ""
actionHref = ""
headerActions = []
navigation = []
activeNav = ""
mobileNavigation = false
alertTitle = ""
alertDescription = ""
empty = false
emptyTitle = "No data to show"
emptyIcon = "icon-[lucide--inbox]"
items = []
size = "md"
color = "primary"
variant = "default"
layout = "vertical"
align = "left"
hover = "none"
scrollable = false
maxHeight = "18rem"
dismissible = false
ariaLabel = ""
class = ""
}
state dismissed = false
view {
<div
{...attrs}
data-ui-component="Card"
data-wrn-card
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
class='wire-next wire-next--card wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}'
>
{#if items.length > 0}
<div class="wire-next__card-group">
{#each items as item, itemIndex}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if item.imageSrc || item.image}
<div class="wire-next__card-media">
<img
src='{item.imageSrc || item.image}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if item.title || item.label}
<h3>{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wire-next__card-subtitle">{item.subtitle}</p>
{/if}
{#if item.description}
<p class="wire-next__card-description">{item.description}</p>
{/if}
{#if item.actionLabel || item.href}
<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 } }))'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</div>
</article>
{/each}
</div>
{:else}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if imageSrc && imagePosition === "overlay"}
<div class="wire-next__card-overlay">
<img
src='{imageSrc}'
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 } }))'
/>
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
</div>
{/if}
{#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if header || title || subtitle || headerActions.length > 0 || dismissible}
<header class="wire-next__card-header">
<div class="wire-next__card-heading">
{#if header}
<p class="wire-next__card-eyebrow">{header}</p>
{/if}
{#if title}
<h3>{title}</h3>
{/if}
{#if subtitle}
<p class="wire-next__card-subtitle">{subtitle}</p>
{/if}
</div>
{#if headerActions.length > 0 || dismissible}
<div class="wire-next__card-header-actions">
{#each headerActions as headerAction, actionIndex}
<button
type="button"
aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { action: headerAction, index: actionIndex } }))'
>
{#if headerAction.icon}
<span class='{headerAction.icon}' aria-hidden="true"></span>
{/if}
{#if headerAction.label && !headerAction.icon}
<span>{headerAction.label}</span>
{/if}
</button>
{/each}
{#if dismissible}
<button
type="button"
aria-label="Dismiss card"
@click='event.currentTarget.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, detail: { title: title } })); dismissed = true'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</div>
{/if}
</header>
{/if}
{#if navigation.length > 0}
<nav aria-label='{ariaLabel || title || "Card navigation"}' class="wire-next__card-navigation">
{#if mobileNavigation}
<select
class="wire-next__card-navigation-select"
aria-label='{ariaLabel || title || "Card navigation"}'
@change='event.currentTarget.dispatchEvent(new CustomEvent("navigate", { bubbles: true, detail: { value: event.target.value } }))'
>
{#each navigation as navItem}
<option
value='{navItem.value || navItem.href || navItem.label}'
selected='{activeNav === navItem.value || activeNav === navItem.href}'
>
{navItem.label}
</option>
{/each}
</select>
{/if}
<div class="wire-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
{#each navigation as navItem, navIndex}
<button
type="button"
aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
@click='event.currentTarget.dispatchEvent(new CustomEvent("navigate", { bubbles: true, detail: { item: navItem, index: navIndex } }))'
>
{#if navItem.icon}
<span class='{navItem.icon}' aria-hidden="true"></span>
{/if}
<span>{navItem.label}</span>
{#if navItem.badge}
<span class="wire-next__card-tab-badge">{navItem.badge}</span>
{/if}
</button>
{/each}
</div>
</nav>
{/if}
{#if alertTitle || alertDescription}
<div class="wire-next__card-alert" role="status">
{#if alertTitle}
<strong>{alertTitle}</strong>
{/if}
{#if alertDescription}
<p>{alertDescription}</p>
{/if}
</div>
{/if}
<div
class="wire-next__card-body"
data-scrollable='{scrollable}'
style='max-height: {scrollable ? maxHeight : "none"};'
>
{#if empty}
<div class="wire-next__card-empty">
<span class='{emptyIcon}' aria-hidden="true"></span>
<p>{emptyTitle}</p>
</div>
{:else}
{#if description}
<p class="wire-next__card-description">{description}</p>
{/if}
<slot></slot>
{/if}
</div>
{#if footer || actionLabel}
<footer class="wire-next__card-footer">
{#if footer}
<span>{footer}</span>
{/if}
{#if actionLabel}
<a
href='{actionHref || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</footer>
{/if}
</div>
{#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
</article>
{/if}
</div>
}
}
@@ -0,0 +1,284 @@
component Card {
props {
@event click = function
@event action = function
@event navigate = function
@event dismiss = function
@event load = function
@event error = function
title = "Card title"
subtitle = ""
description = ""
header = ""
footer = ""
imageSrc = ""
imageAlt = ""
imagePosition = "top"
actionLabel = ""
actionHref = ""
headerActions = []
navigation = []
activeNav = ""
mobileNavigation = false
alertTitle = ""
alertDescription = ""
empty = false
emptyTitle = "No data to show"
emptyIcon = "icon-[lucide--inbox]"
items = []
size = "md"
color = "primary"
variant = "default"
layout = "vertical"
align = "left"
hover = "none"
scrollable = false
maxHeight = "18rem"
dismissible = false
ariaLabel = ""
class = ""
}
state dismissed = false
view {
<div
{...attrs}
data-ui-component="Card"
data-wrn-card
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
class='wire-next wire-next--card wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}'
>
{#if items.length > 0}
<div class="wire-next__card-group">
{#each items as item, itemIndex}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if item.imageSrc || item.image}
<div class="wire-next__card-media">
<img
src='{item.imageSrc || item.image}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if item.title || item.label}
<h3>{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wire-next__card-subtitle">{item.subtitle}</p>
{/if}
{#if item.description}
<p class="wire-next__card-description">{item.description}</p>
{/if}
{#if item.actionLabel || item.href}
<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 } }))'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</div>
</article>
{/each}
</div>
{:else}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
data-show='!dismissed'
>
{#if imageSrc && imagePosition === "overlay"}
<div class="wire-next__card-overlay">
<img
src='{imageSrc}'
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 } }))'
/>
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
</div>
{/if}
{#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if header || title || subtitle || headerActions.length > 0 || dismissible}
<header class="wire-next__card-header">
<div class="wire-next__card-heading">
{#if header}
<p class="wire-next__card-eyebrow">{header}</p>
{/if}
{#if title}
<h3>{title}</h3>
{/if}
{#if subtitle}
<p class="wire-next__card-subtitle">{subtitle}</p>
{/if}
</div>
{#if headerActions.length > 0 || dismissible}
<div class="wire-next__card-header-actions">
{#each headerActions as headerAction, actionIndex}
<button
type="button"
aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { action: headerAction, index: actionIndex } }))'
>
{#if headerAction.icon}
<span class='{headerAction.icon}' aria-hidden="true"></span>
{/if}
{#if headerAction.label && !headerAction.icon}
<span>{headerAction.label}</span>
{/if}
</button>
{/each}
{#if dismissible}
<button
type="button"
aria-label="Dismiss card"
@click='event.currentTarget.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, detail: { title: title } })); dismissed = true'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</div>
{/if}
</header>
{/if}
{#if navigation.length > 0}
<nav aria-label='{ariaLabel || title || "Card navigation"}' class="wire-next__card-navigation">
{#if mobileNavigation}
<select
class="wire-next__card-navigation-select"
aria-label='{ariaLabel || title || "Card navigation"}'
@change='event.currentTarget.dispatchEvent(new CustomEvent("navigate", { bubbles: true, detail: { value: event.target.value } }))'
>
{#each navigation as navItem}
<option
value='{navItem.value || navItem.href || navItem.label}'
selected='{activeNav === navItem.value || activeNav === navItem.href}'
>
{navItem.label}
</option>
{/each}
</select>
{/if}
<div class="wire-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
{#each navigation as navItem, navIndex}
<button
type="button"
aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
@click='event.currentTarget.dispatchEvent(new CustomEvent("navigate", { bubbles: true, detail: { item: navItem, index: navIndex } }))'
>
{#if navItem.icon}
<span class='{navItem.icon}' aria-hidden="true"></span>
{/if}
<span>{navItem.label}</span>
{#if navItem.badge}
<span class="wire-next__card-tab-badge">{navItem.badge}</span>
{/if}
</button>
{/each}
</div>
</nav>
{/if}
{#if alertTitle || alertDescription}
<div class="wire-next__card-alert" role="status">
{#if alertTitle}
<strong>{alertTitle}</strong>
{/if}
{#if alertDescription}
<p>{alertDescription}</p>
{/if}
</div>
{/if}
<div
class="wire-next__card-body"
data-scrollable='{scrollable}'
style='max-height: {scrollable ? maxHeight : "none"};'
>
{#if empty}
<div class="wire-next__card-empty">
<span class='{emptyIcon}' aria-hidden="true"></span>
<p>{emptyTitle}</p>
</div>
{:else}
{#if description}
<p class="wire-next__card-description">{description}</p>
{/if}
<slot></slot>
{/if}
</div>
{#if footer || actionLabel}
<footer class="wire-next__card-footer">
{#if footer}
<span>{footer}</span>
{/if}
{#if actionLabel}
<a
href='{actionHref || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</footer>
{/if}
</div>
{#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
</article>
{/if}
</div>
}
}
@@ -0,0 +1,203 @@
component Accordion {
props {
size = "default"
color = "primary"
variant = "default"
class = ""
id = "accordion"
items = []
defaultOpen = []
multiple = false
alwaysOpen = false
disabled = false
indicator = "plus"
indicatorPosition = "start"
showIndicator = true
bordered = false
separated = false
flush = false
contentItalic = false
@event change = function
@event open = function
@event close = function
}
state openValues = defaultOpen
functions {
function itemValue(item, index) {
return item.value !== undefined && item.value !== ""
? String(item.value)
: String(index)
}
function nestedValue(parent, parentIndex, item, index) {
return itemValue(parent, parentIndex) + "." + itemValue(item, index)
}
function isOpen(value) {
return openValues.includes(value)
}
function allowsMultiple() {
return multiple || alwaysOpen
}
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, {
component: "Accordion",
value: value,
item: item,
open: isOpen(value),
openValues: openValues
})
root.dispatchEvent(customEvent)
}
function toggleItem(sourceEvent, value, item, wasOpen) {
if (disabled || item.disabled) {
return
}
wasOpen = isOpen(value)
if (wasOpen) {
openValues = openValues.filter((entry) => entry !== value)
} else if (allowsMultiple()) {
openValues = openValues.concat([value])
} else {
openValues = [value]
}
dispatchAccordionEvent(
sourceEvent,
wasOpen ? "close" : "open",
value,
item
)
dispatchAccordionEvent(sourceEvent, "change", value, item)
}
}
view {
<div
{...attrs}
id="{id}"
data-wrn-accordion
data-variant="{variant}"
data-indicator="{indicator}"
data-multiple="{allowsMultiple() ? 'true' : 'false'}"
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--accordion wire-next--accordion-{variant} {bordered ? 'wire-next--accordion-bordered' : ''} {separated ? 'wire-next--accordion-separated' : ''} {flush ? 'wire-next--accordion-flush' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
>
{#each items as item, index}
<section
class="wire-next__accordion-item"
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
>
<h3 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}"
aria-expanded="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}"
disabled="{disabled || item.disabled}"
@click="toggleItem(event, itemValue(item, index), item)"
>
{#if showIndicator && indicatorPosition === "start"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{item.label || item.title}</span>
{#if showIndicator && indicatorPosition === "end"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
</button>
</h3>
<div
id="{id}-panel-{index}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}"
aria-hidden="{isOpen(itemValue(item, index)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
{#if item.content}<p>{item.content}</p>{/if}
{#if item.children && item.children.length > 0}
<div class="wire-next__accordion-nested">
{#each item.children as child, childIndex}
<section
class="wire-next__accordion-item"
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
>
<h4 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}-{childIndex}"
aria-expanded="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}-{childIndex}"
disabled="{disabled || child.disabled}"
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
>
{#if showIndicator}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{child.label || child.title}</span>
</button>
</h4>
<div
id="{id}-panel-{index}-{childIndex}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}-{childIndex}"
aria-hidden="{isOpen(nestedValue(item, index, child, childIndex)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
<p>{child.content}</p>
</div>
</div>
</div>
</section>
{/each}
</div>
{/if}
</div>
</div>
</div>
</section>
{/each}
</div>
}
}
@@ -0,0 +1,24 @@
component AdvancedDatePicker {
props {
@event input = function
@event change = function
@event open = function
@event close = function
@event clear = function
size = "default"
color = "primary"
title = "Advanced Date Picker"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-date-picker wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,23 @@
component AdvancedRangeSlider {
props {
@event input = function
@event change = function
@event start = function
@event end = function
size = "default"
color = "primary"
title = "Advanced Range Slider"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-range-slider wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,456 @@
component AdvancedSelect {
props {
size = "default"
color = "primary"
label = "Advanced Select"
name = ""
value = ""
values = []
options = []
groups = []
placeholder = "Select an option"
placeholderIcon = ""
searchPlaceholder = "Search options…"
multiple = false
searchable = true
defaultOpen = false
clearable = true
allowEmpty = true
tags = false
disabled = false
required = false
invalid = false
validationMessage = ""
helpText = ""
loading = false
loadingLabel = "Loading options…"
emptyLabel = "No options found"
selectedOptionsLabel = "Selected options"
clearLabel = "Clear selection"
createLabel = "Create"
loadMoreLabel = "Load more"
searchMode = "contains"
searchFields = "label,description"
minSearchLength = 0
searchResultLimit = 0
maxSelections = 0
showCounter = false
counterTemplate = "{selected} selected"
optionTemplate = "default"
selectedTemplate = "default"
closeOnSelect = true
scrollToSelected = true
fixed = false
placement = "bottom"
remote = false
remoteUrl = ""
remoteQueryParam = "q"
remoteDebounce = 250
remoteAutoLoad = true
infinite = false
hasMore = false
page = 1
class = ""
@event search = function
@event select = function
@event change = function
@event clear = function
@event open = function
@event close = function
@event load = function
@event error = function
}
state open = defaultOpen
state query = ""
state activeIndex = -1
state selectedValue = value
state selectedValues = values
functions {
function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
function matches(option) {
if (!query || query.length < Number(minSearchLength || 0)) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
function matchingOptions(list) {
return list.filter((option) => matches(option))
}
function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, Number(searchResultLimit))
}
return matchingOptions(list)
}
function flatVisibleOptions() {
return visibleOptions(allOptions())
}
function isSelected(option) {
return (
multiple
? selectedValues.includes(option.value)
: selectedValue === option.value
)
}
function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
function selectedCount() {
return multiple ? selectedValues.length : selectedValue ? 1 : 0
}
function counterText() {
return (
maxSelections
? selectedCount() + " / " + maxSelections + " selected"
: selectedCount() + " selected"
)
}
function selectedText() {
return (
multiple
? selectedValues.join(", ")
: selectedOptions().length
? selectedOptions()[0].label
: ""
)
}
function triggerText() {
return selectedCount() ? selectedText() : placeholder
}
function canSelect(option) {
if (option.disabled) {
return false
}
if (!multiple || isSelected(option)) {
return true
}
if (maxSelections <= 0) {
return true
}
return selectedCount() < maxSelections
}
function chooseSingle(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = ""
if (closeOnSelect) {
open = false
}
}
function chooseMultiple(option) {
if (!canSelect(option)) {
return
}
if (isSelected(option)) {
selectedValues = selectedValues.filter((item) => item !== option.value)
} else {
selectedValues = selectedValues.concat([option.value])
}
query = ""
}
function chooseOption(option) {
if (multiple) {
chooseMultiple(option)
return
}
chooseSingle(option)
}
function clearSelection(event) {
event.stopPropagation()
selectedValue = ""
selectedValues = []
query = ""
open = false
}
function toggle() {
if (disabled) {
return;
};
open = !open;
activeIndex = open && flatVisibleOptions().length ? 0 : -1;
}
function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(-1)
} else if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
if (!open) {
open = true
} else if (activeIndex >= 0) {
chooseOption(flatVisibleOptions()[activeIndex])
}
} else if (event.key === "Escape") {
open = false
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-select {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
>
<div class="wire-next__row">
<label id="{name}-label" for="{name}-trigger">{label}</label>
<small data-show="showCounter" data-text="counterText()">{counterText()}</small>
</div>
<div class="wire-next__select-control">
<button
{...attrs}
id="{name}-trigger"
type="button"
class="wire-next__select-trigger"
role="combobox"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
@click="toggle()"
@keydown="handleKeydown(event)"
>
<span class="wire-next__select-value">
<span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
<span
data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
data-text="selectedCount() + ' selected'"
>{selectedCount()} selected</span>
<span
data-show="multiple && selectedTemplate === 'text' && selectedCount() > 0"
data-text="selectedText()"
>{selectedText()}</span>
<span
class="wire-next__select-tags"
aria-label="{selectedOptionsLabel}"
data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
>
{#each allOptions() as option}
<span data-show="isSelected(option)">
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
{option.label}
</span>
{/each}
</span>
<span data-show="multiple && selectedCount() === 0">
{#if placeholderIcon}<i class="{placeholderIcon}" aria-hidden="true"></i>{/if}
<span class="wire-next__placeholder">{placeholder}</span>
</span>
</span>
<span
class="wire-next__select-chevron icon-[lucide--chevrons-up-down]"
aria-hidden="true"
></span>
</button>
<button
type="button"
class="wire-next__clear-select"
data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearSelection(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
</div>
<div
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
data-show="open"
style="{open ? '' : 'display: none'}"
>
{#if searchable}
<label class="wire-next__select-search">
<span class="wire-visually-hidden">{searchPlaceholder}</span>
<span class="wire-next__search-icon" aria-hidden="true">⌕</span>
<input
type="search"
value="{query}"
placeholder="{searchPlaceholder}"
autocomplete="off"
@input="query = event.target.value; activeIndex = flatVisibleOptions().length ? 0 : -1"
@keydown="handleKeydown(event)"
/>
</label>
{/if}
<div
class="wire-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<div class="wire-next__select-message" data-show="loading" role="status">
<i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
role="listbox"
aria-multiselectable="{multiple}"
>
{#each groups as group}
{#if visibleOptions(group.options || []).length}
<div class="wire-next__option-group" role="group" aria-label="{group.label}">
<div class="wire-next__group-label">{group.label}</div>
{#each group.options || [] as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i
class="wire-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i
class="wire-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
{#if !loading && !flatVisibleOptions().length}
<div class="wire-next__select-message">{emptyLabel}</div>
{/if}
</div>
{#if tags && query && !allOptions().some((option) => option.label.toLowerCase() === query.toLowerCase())}
<button type="button" class="wire-next__create-option">{createLabel} “{query}”</button>
{/if}
<button
type="button"
class="wire-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
<input
type="hidden"
name="{name}"
value="{multiple ? selectedValues.join(',') : selectedValue}"
required="{required}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
}
}
@@ -0,0 +1,131 @@
component AnnouncementBar {
props {
badge = ""
badgeIcon = ""
message = "Announcement"
description = ""
icon = "icon-[lucide--megaphone]"
actionLabel = ""
actionHref = ""
actionIcon = ""
dismissible = false
dismissLabel = "Dismiss announcement"
sticky = false
compact = false
size = "default"
color = "primary"
variant = "soft"
role = "status"
live = "polite"
class = ""
}
state dismissed = false
view {
<aside
data-ui-component="AnnouncementBar"
role='{role}'
aria-live='{live}'
data-show='!dismissed'
class='z-40 w-full border-y border-[var(--wire-color-border)] {class}'
class:sticky='sticky'
class:top-0='sticky'
class:bg-[var(--wire-color-primary-soft)]='variant === "soft" && color === "primary"'
class:border-[var(--wire-color-primary-muted)]='variant === "soft" && color === "primary"'
class:bg-[var(--wire-color-info-soft)]='variant === "soft" && color === "info"'
class:border-[var(--wire-color-info-muted)]='variant === "soft" && color === "info"'
class:bg-[var(--wire-color-success-soft)]='variant === "soft" && color === "success"'
class:border-[var(--wire-color-success-muted)]='variant === "soft" && color === "success"'
class:bg-[var(--wire-color-warning-soft)]='variant === "soft" && color === "warning"'
class:border-[var(--wire-color-warning-muted)]='variant === "soft" && color === "warning"'
class:bg-[var(--wire-color-danger-soft)]='variant === "soft" && color === "danger"'
class:border-[var(--wire-color-danger-muted)]='variant === "soft" && color === "danger"'
class:bg-[var(--wire-color-primary)]='variant === "solid" && color === "primary"'
class:text-[var(--wire-color-on-primary)]='variant === "solid" && color === "primary"'
class:bg-[var(--wire-color-danger)]='variant === "solid" && color === "danger"'
class:text-[var(--wire-color-on-danger)]='variant === "solid" && color === "danger"'
class:bg-[var(--wire-color-surface-raised)]='variant === "default"'
>
<Container columns="1" maxWidth="xl" size="default">
<div
class="flex flex-col gap-4 py-4 sm:flex-row sm:items-center sm:justify-between"
class:py-2.5='compact'
>
<div class="flex min-w-0 items-start gap-3 sm:items-center">
{#if icon}
<span
class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-primary)] shadow-sm"
class:size-8='compact'
class:text-[var(--wire-color-danger)]='color === "danger"'
class:text-[var(--wire-color-warning-text)]='color === "warning"'
class:text-[var(--wire-color-success)]='color === "success"'
class:text-[var(--wire-color-info)]='color === "info"'
>
<span class='{icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
{#if badge}
<Badge
label='{badge}'
icon='{badgeIcon}'
size="sm"
color='{color}'
variant='{variant === "solid" ? "outline" : "solid"}'
/>
{/if}
<p
class="font-semibold"
class:text-sm='compact || size === "sm"'
class:text-base='!compact && size !== "sm"'
class:text-[var(--wire-color-text)]='variant !== "solid"'
>
{message}
</p>
</div>
{#if description && !compact}
<p
class="mt-1 text-sm leading-6"
class:text-[var(--wire-color-text-muted)]='variant !== "solid"'
class:opacity-85='variant === "solid"'
>
{description}
</p>
{/if}
</div>
</div>
<div class="flex shrink-0 items-center gap-2 sm:justify-end">
{#if actionLabel}
<TextLink
label='{actionLabel}'
href='{actionHref}'
icon='{actionIcon}'
iconPosition="start"
showArrow="true"
color='{variant === "solid" ? "neutral" : color}'
variant='{variant === "solid" ? "button" : "default"}'
/>
{/if}
{#if dismissible}
<button
type="button"
aria-label='{dismissLabel}'
class="inline-flex size-9 items-center justify-center rounded-lg text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-raised)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='dismissed = true; event.currentTarget.dispatchEvent(new CustomEvent("dismiss", { bubbles: true }))'
>
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
</button>
{/if}
</div>
</div>
</Container>
</aside>
}
}
@@ -0,0 +1,84 @@
component AuthForm {
props {
@event submit = function
@event change = function
@event input = function
@event focus = function
@event blur = function
size = "default"
color = "primary"
mode = "sign-in"
action = "/api/auth/login"
method = "post"
title = "Sign in"
description = ""
returnTo = ""
schema = ""
showRemember = true
showName = true
submitLabel = "Continue"
class = ""
}
functions {
function schemaName() {
if (schema) return schema
if (mode === "sign-in") return "auth-login"
if (mode === "register") return "auth-register"
if (mode === "recover") return "auth-password-request"
if (mode === "reset") return "auth-password-reset"
if (mode === "mfa") return "auth-mfa"
return "auth-empty"
}
function submitForm(event) {
$emit("submit", { event: event, mode: mode, action: action })
}
function fieldEvent(type, event) {
const detail = event.detail || {}
$emit(type, { event: event, name: detail.name || "", value: detail.value || "" })
}
}
view {
<section class="wire-auth-form wire-next--color-{color} wire-next--size-{size} {class}">
<header>
<span class="wire-auth-form__icon icon-[lucide--fingerprint]" aria-hidden="true"></span>
<div><h2>{title}</h2>{#if description}<p>{description}</p>{/if}</div>
</header>
<form
action="{action}"
method="{method}"
data-schema="{schemaName()}"
data-wrnexus-runtime="auth"
novalidate="true"
@submit="submitForm($event)"
>
{#if returnTo}<input type="hidden" name="returnTo" value="{returnTo}" />{/if}
{#if mode === "register" && showName}
<Input label="Full name" name="displayName" autocomplete="name" placeholder="Enter your full name" required="true" icon="icon-[lucide--user-round]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
{/if}
{#if mode === "sign-in" || mode === "register" || mode === "recover"}
<Input label="{mode === 'recover' ? 'Registered email or mobile' : 'Email, mobile, or username'}" name="{mode === 'recover' ? 'identifier' : mode === 'register' ? 'email' : 'identifier'}" type="{mode === 'register' ? 'email' : 'text'}" autocomplete="{mode === 'register' ? 'email' : 'username'}" placeholder="{mode === 'recover' ? 'Enter your registered identity' : 'Enter your identity'}" required="true" icon="icon-[lucide--at-sign]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
{/if}
{#if mode === "sign-in" || mode === "register" || mode === "reset"}
<Input label="{mode === 'reset' ? 'New password' : 'Password'}" name="password" type="password" autocomplete="{mode === 'sign-in' ? 'current-password' : 'new-password'}" placeholder="Enter your password" required="true" icon="icon-[lucide--lock-keyhole]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
{/if}
{#if mode === "register" || mode === "reset"}
<Input label="Confirm password" name="confirmPassword" type="password" autocomplete="new-password" placeholder="Enter the password again" required="true" icon="icon-[lucide--shield-check]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
{/if}
{#if mode === "mfa"}
<PinInput label="Verification code" name="code" length={6} inputMode="numeric" />
{/if}
{#if mode === "sign-in" && showRemember}
<div class="wire-auth-form__options">
<Checkbox label="Remember this device" name="rememberDevice" value="on" />
<a href="/forgot-password">Forgot password?</a>
</div>
{/if}
<Button type="submit" label="{submitLabel}" variant="default" color="{color}" size="lg" fullWidth="true" icon="icon-[lucide--arrow-right]" iconPosition="end" class="wire-auth-form__submit" />
</form>
<slot />
</section>
}
}
@@ -0,0 +1,83 @@
component AuthSplitLayout {
props {
size = "default"
color = "primary"
eyebrow = "Secure identity"
title = "Welcome back"
description = ""
brand = "Police Management System"
features = []
class = ""
}
view {
<main
class="wire-auth-split wire-next--color-{color} wire-next--size-{size} {class}"
>
<section
class="wire-auth-split__content"
>
<div
class="wire-auth-split__brand"
>
<span
class="icon-[lucide--shield-check]"
aria-hidden="true"
>
</span>
<span><strong>{brand}</strong><small>Unified identity and access</small></span>
</div>
<div
class="wire-auth-split__message"
>
<span
class="wire-auth-split__eyebrow"
>
{eyebrow}
</span>
<h1>{title}</h1>
{#if description}
<p>{description}</p>
{/if}
<div
class="wire-auth-split__features"
>
{#each features as item}
<article>
<span
class="{item.icon || 'icon-[lucide--check-circle-2]'}"
aria-hidden="true"
>
</span>
<span><strong>{item.label}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
</article>
{/each}
</div>
<slot
name="aside-extra"
/>
</div>
<small
class="wire-auth-split__legal"
>
Authorised access only · Activity may be monitored and audited.
</small>
</section>
<section
class="wire-auth-split__form"
>
<div
class="wire-auth-split__form-inner"
>
<slot
name="form"
/>
</div>
</section>
</main>
}
}
@@ -0,0 +1,151 @@
component AvatarGroup {
props {
items = []
size = "md"
color = "primary"
variant = "solid"
shape = "circle"
layout = "stack"
maxVisible = 4
columns = 3
borderColor = ""
showTooltips = true
overflowLabel = "Show remaining members"
class = ""
@event overflow = function
}
state overflowOpen = false
functions {
function visibleMembers() {
return items.slice(0, Number(maxVisible))
}
function hiddenMembers() {
return items.slice(Number(maxVisible))
}
function toggleOverflow(sourceEvent, root, customEvent) {
overflowOpen = !overflowOpen
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("overflow", true, false, {
component: "AvatarGroup",
open: overflowOpen,
hiddenCount: hiddenMembers().length
})
root.dispatchEvent(customEvent)
}
}
view {
<div
{...attrs}
class="wire-next wire-next--avatar-group {class}"
data-wrn-avatar-group
data-layout="{layout}"
data-shape="{shape}"
data-size="{size}"
style="--wire-avatar-group-columns:{columns}; --wire-avatar-group-ring:{borderColor || 'var(--wire-color-bg)'}"
role="group"
aria-label="Avatar group"
>
<div
class="wire-next__avatar-group-members"
>
{#each visibleMembers() as item}
<span
class="wire-next__avatar-group-member"
data-size="{item.size || size}"
data-shape="{item.shape || shape}"
data-color="{item.color || color}"
data-variant="{item.variant || variant}"
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
>
<span
class="wire-next__avatar-group-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt="{item.alt || item.name || ''}"
loading="lazy"
/>
{:else if item.initials}
<span
aria-hidden="true"
>
{item.initials}
</span>
{:else}
<span
class="icon-[lucide--user-round]"
aria-hidden="true"
>
</span>
{/if}
</span>
{#if showTooltips && (item.tooltip || item.name)}
<span
class="wire-next__avatar-group-tooltip"
role="tooltip"
>
{item.tooltip || item.name}
</span>
{/if}
</span>
{/each}
{#if hiddenMembers().length > 0}
<span
class="wire-next__avatar-group-overflow"
>
<button
type="button"
class="wire-next__avatar-group-overflow-button"
aria-label="{overflowLabel}"
aria-expanded="{overflowOpen ? 'true' : 'false'}"
@click="toggleOverflow(event)"
>
+{hiddenMembers().length}
</button>
<span
class="wire-next__avatar-group-menu"
role="menu"
data-show="overflowOpen"
aria-hidden="{overflowOpen ? 'false' : 'true'}"
>
{#each hiddenMembers() as item}
<span
class="wire-next__avatar-group-menu-item"
role="menuitem"
>
<span
class="wire-next__avatar-group-menu-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt=""
loading="lazy"
/>
{:else}
<span>{item.initials || '?'}</span>
{/if}
</span>
<span>{item.name || item.alt || item.initials || 'Team member'}</span>
</span>
{/each}
</span>
</span>
{/if}
</div>
</div>
}
}
@@ -0,0 +1,69 @@
component BackToTop {
props {
threshold = 500
label = "Back to top"
ariaLabel = "Scroll back to top"
icon = "icon-[lucide--arrow-up]"
position = "right"
offset = "md"
behavior = "smooth"
showProgress = false
size = "default"
color = "primary"
variant = "solid"
class = ""
}
state visible = false
state progress = 0
view {
<button
data-ui-component="BackToTop"
type="button"
aria-label='{ariaLabel}'
title='{label}'
data-show='visible'
class='fixed z-50 inline-flex items-center justify-center rounded-full border border-[var(--wire-color-border)] shadow-xl backdrop-blur transition duration-200 hover:-translate-y-1 focus-visible:outline-none 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:bottom-4='offset === "sm"'
class:bottom-6='offset === "md"'
class:bottom-8='offset === "lg"'
class:right-4='position === "right" && offset === "sm"'
class:right-6='position === "right" && offset === "md"'
class:right-8='position === "right" && offset === "lg"'
class:left-4='position === "left" && offset === "sm"'
class:left-6='position === "left" && offset === "md"'
class:left-8='position === "left" && offset === "lg"'
class:left-1/2='position === "center"'
class:-translate-x-1/2='position === "center"'
class:size-10='size === "sm"'
class:size-12='size === "default" || size === "md"'
class:size-14='size === "lg"'
class:bg-[var(--wire-color-primary)]='variant === "solid" && color === "primary"'
class:text-[var(--wire-color-on-primary)]='variant === "solid" && color === "primary"'
class:bg-[var(--wire-color-danger)]='variant === "solid" && color === "danger"'
class:text-[var(--wire-color-on-danger)]='variant === "solid" && color === "danger"'
class:bg-[var(--wire-color-surface-raised)]='variant === "outline" || variant === "soft"'
class:text-[var(--wire-color-primary)]='variant === "outline" || variant === "soft"'
style='--wire-back-to-top-progress: {progress}%;'
@window:scroll='visible = window.scrollY >= threshold; progress = Math.min(100, Math.round((window.scrollY / Math.max(1, document.documentElement.scrollHeight - window.innerHeight)) * 100))'
@click='window.scrollTo({ top: 0, behavior: behavior })'
>
{#if showProgress}
<span class="wire-back-to-top-progress absolute inset-0 rounded-full" aria-hidden="true"></span>
{/if}
<span class='{icon + " relative z-10 size-5"}' aria-hidden="true"></span>
<span class="sr-only">{label}</span>
</button>
}
style {
.wire-back-to-top-progress {
background: conic-gradient(
currentColor var(--wire-back-to-top-progress),
transparent var(--wire-back-to-top-progress)
);
opacity: 0.22;
}
}
}
@@ -0,0 +1,102 @@
component Badge {
props {
label = "Badge"
size = "md"
color = "primary"
variant = "solid"
shape = "pill"
class = ""
icon = ""
iconPosition = "start"
dot = false
dotOnly = false
dotLabel = "Status"
animated = false
avatarSrc = ""
avatarAlt = ""
dismissible = false
dismissLabel = "Remove badge"
truncate = false
maxWidth = "12rem"
anchorLabel = ""
anchorIcon = ""
placement = "inline"
anchorLabelText = "Badge anchor"
@event dismiss = function
}
state visible = true
functions {
function dismissBadge(sourceEvent, root, customEvent) {
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)
}
}
view {
<span
{...attrs}
class="wire-next wire-next--badge-root {anchorLabel || anchorIcon ? 'wire-next--badge-anchored' : ''} {class}"
data-wrn-badge
data-placement="{placement}"
data-show="visible"
aria-hidden="{visible ? 'false' : 'true'}"
>
{#if anchorLabel || anchorIcon}
<button type="button" class="wire-next__badge-anchor" aria-label="{anchorLabelText}">
{#if anchorIcon}<span class="{anchorIcon}" aria-hidden="true"></span>{/if}
{#if anchorLabel}<span>{anchorLabel}</span>{/if}
</button>
{/if}
<span
class="wire-next__badge"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-shape="{shape}"
data-animated="{animated ? 'true' : 'false'}"
style="--wire-badge-max-width:{maxWidth}"
role="{dotOnly ? 'status' : ''}"
aria-label="{dotOnly ? dotLabel : ''}"
>
{#if animated}
<span class="wire-next__badge-ping" aria-hidden="true"></span>
{/if}
{#if avatarSrc}
<img class="wire-next__badge-avatar" src="{avatarSrc}" alt="{avatarAlt}" loading="lazy" />
{/if}
{#if dot || dotOnly}
<span class="wire-next__badge-dot" aria-hidden="true"></span>
{/if}
{#if icon && iconPosition === "start"}
<span class="wire-next__badge-icon {icon}" aria-hidden="true"></span>
{/if}
{#if !dotOnly}
<span class="wire-next__badge-label {truncate ? 'wire-next__badge-label--truncate' : ''}">
{label}
</span>
{/if}
{#if icon && iconPosition === "end"}
<span class="wire-next__badge-icon {icon}" aria-hidden="true"></span>
{/if}
{#if dismissible}
<button type="button" class="wire-next__badge-dismiss" aria-label="{dismissLabel}" @click="dismissBadge(event)">
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</span>
</span>
}
}
@@ -0,0 +1,67 @@
component Blockquote {
props {
quote = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."
citation = ""
citationTitle = ""
citationUrl = ""
avatarSrc = ""
avatarAlt = ""
size = "md"
color = "primary"
align = "left"
variant = "default"
quoteMark = true
italic = true
class = ""
}
view {
<figure
{...attrs}
class="wire-next wire-next--blockquote wire-next--color-{color} wire-next--size-{size} {class}"
data-size="{size}"
data-color="{color}"
data-align="{align}"
data-variant="{variant}"
data-italic="{italic}"
>
<blockquote cite="{citationUrl}">
{#if quoteMark}
<span class="wire-next__blockquote-mark" aria-hidden="true">“</span>
{/if}
<div class="wire-next__blockquote-copy">
{#if quote}
<p>{quote}</p>
{:else}
<slot />
{/if}
</div>
</blockquote>
{#if citation || citationTitle || avatarSrc}
<figcaption class="wire-next__blockquote-citation">
{#if avatarSrc}
<img
class="wire-next__blockquote-avatar"
src="{avatarSrc}"
alt="{avatarAlt}"
loading="lazy"
/>
{/if}
<span class="wire-next__blockquote-attribution">
{#if citation}
{#if citationUrl}
<cite><a href="{citationUrl}">{citation}</a></cite>
{:else}
<cite>{citation}</cite>
{/if}
{/if}
{#if citationTitle}<span>{citationTitle}</span>{/if}
</span>
</figcaption>
{/if}
</figure>
}
}
@@ -0,0 +1,19 @@
component Breadcrumb {
props {
@event navigate = function
@event click = function
size = "default"
color = "primary"
label = "Breadcrumb"
items = []
active = ""
orientation = "horizontal"
class = ""
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--breadcrumb 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>
}
}
@@ -0,0 +1,89 @@
component ButtonGroup {
props {
@event click = function
@event select = function
@event change = function
items = []
value = ""
size = "md"
color = "primary"
variant = "default"
orientation = "horizontal"
responsive = false
attached = true
selectable = false
toolbar = false
disabled = false
ariaLabel = "Button group"
class = ""
}
state selectedValue = value
functions {
function itemValue(item, index) {
return item.value || item.label || String(index)
}
function selectItem(item, index) {
previousValue = selectedValue
nextValue = itemValue(item, index)
$emit("select", {
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
if (selectable && previousValue !== nextValue) {
selectedValue = nextValue
$emit("change", {
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
}
}
}
view {
<div
{...attrs}
class="wire-next wire-next--button-group {class}"
role="{toolbar ? 'toolbar' : 'group'}"
aria-label="{ariaLabel}"
aria-orientation="{orientation}"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-orientation="{orientation}"
data-responsive="{responsive}"
data-attached="{attached}"
data-selectable="{selectable}"
data-disabled="{disabled}"
>
{#if items}
{#each items as item, index}
<button
type="{item.type || 'button'}"
class="wire-btn wire-btn--variant-{item.variant || variant} wire-btn--color-{item.color || color} wire-btn--size-{item.size || size}"
disabled="{disabled || item.disabled}"
aria-label="{item.ariaLabel || item.label}"
aria-pressed="{selectable ? (selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false') : ''}"
data-value="{item.value || item.label || String(index)}"
data-selected="{selectable && selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false'}"
@click="selectItem(item, index)"
>
{#if item.icon}
<span class="wire-btn__icon {item.icon}" aria-hidden="true"></span>
{/if}
<span class="wire-btn__label">{item.label}</span>
</button>
{/each}
{/if}
<slot />
</div>
}
}
@@ -0,0 +1,150 @@
component CTASection {
props {
eyebrow = ""
title = "Ready to get started?"
description = ""
icon = ""
align = "center"
size = "default"
color = "primary"
variant = "solid"
primaryLabel = "Get started"
primaryHref = "#"
primaryIcon = ""
secondaryLabel = ""
secondaryHref = ""
secondaryIcon = ""
backgroundImage = ""
maxWidth = "xl"
class = ""
}
view {
<Section
spacing='{size === "compact" ? "md" : "lg"}'
variant="default"
color='{color}'
maxWidth='{maxWidth}'
class='{class}'
>
<div
data-ui-component="CTASection"
class="relative isolate overflow-hidden rounded-3xl border border-[var(--wire-color-border)] p-8 shadow-xl sm:p-10 lg:p-14"
class:bg-[var(--wire-color-primary)]='variant === "solid" && color === "primary"'
class:text-[var(--wire-color-on-primary)]='variant === "solid" && color === "primary"'
class:bg-[var(--wire-color-danger)]='variant === "solid" && color === "danger"'
class:text-[var(--wire-color-on-danger)]='variant === "solid" && color === "danger"'
class:bg-[var(--wire-color-primary-soft)]='variant === "soft" && color === "primary"'
class:bg-[var(--wire-color-success-soft)]='variant === "soft" && color === "success"'
class:bg-[var(--wire-color-warning-soft)]='variant === "soft" && color === "warning"'
class:bg-[var(--wire-color-danger-soft)]='variant === "soft" && color === "danger"'
class:bg-[var(--wire-color-surface-raised)]='variant === "default" || variant === "outline"'
class:bg-gradient-to-br='variant === "gradient"'
class:from-[var(--wire-color-primary)]='variant === "gradient"'
class:to-[var(--wire-color-secondary)]='variant === "gradient"'
class:text-[var(--wire-color-on-primary)]='variant === "gradient"'
class:text-center='align === "center"'
>
{#if backgroundImage}
<img
src='{backgroundImage}'
alt=""
aria-hidden="true"
class="absolute inset-0 -z-20 h-full w-full object-cover opacity-15"
/>
{/if}
<div
class="pointer-events-none absolute -right-20 -top-20 -z-10 size-72 rounded-full bg-white/15 blur-3xl"
aria-hidden="true"
></div>
<div
class="pointer-events-none absolute -bottom-24 -left-20 -z-10 size-72 rounded-full bg-black/10 blur-3xl"
aria-hidden="true"
></div>
<div
class="flex flex-col gap-8"
class:items-center='align === "center"'
class:lg:flex-row='align === "split"'
class:lg:items-center='align === "split"'
class:lg:justify-between='align === "split"'
>
<div class="max-w-3xl">
{#if icon}
<span
class="mb-5 inline-flex size-12 items-center justify-center rounded-2xl bg-white/15"
class:bg-[var(--wire-color-primary-soft)]='variant !== "solid" && variant !== "gradient"'
class:text-[var(--wire-color-primary)]='variant !== "solid" && variant !== "gradient"'
>
<span class='{icon + " size-6"}' aria-hidden="true"></span>
</span>
{/if}
{#if eyebrow}
<p
class="text-xs font-bold uppercase tracking-[0.18em]"
class:text-[var(--wire-color-primary)]='variant !== "solid" && variant !== "gradient"'
class:opacity-80='variant === "solid" || variant === "gradient"'
>
{eyebrow}
</p>
{/if}
<h2
class="mt-3 text-3xl font-bold leading-tight tracking-tight sm:text-4xl lg:text-5xl"
class:text-[var(--wire-color-text)]='variant !== "solid" && variant !== "gradient"'
>
{title}
</h2>
{#if description}
<p
class="mt-4 text-base leading-7 sm:text-lg"
class:text-[var(--wire-color-text-muted)]='variant !== "solid" && variant !== "gradient"'
class:opacity-85='variant === "solid" || variant === "gradient"'
>
{description}
</p>
{/if}
<slot></slot>
</div>
<HeroActions
actions='{[
{
label: primaryLabel,
href: primaryHref,
icon: primaryIcon,
variant: "default",
color: color,
controlClass: variant === "solid" || variant === "gradient" ? "!bg-white !text-[var(--wire-color-primary)] hover:!bg-white/90" : ""
},
{
label: secondaryLabel,
href: secondaryHref,
icon: secondaryIcon,
variant: "outline",
color: color,
controlClass: variant === "solid" || variant === "gradient" ? "!border-white/50 !text-white hover:!bg-white/10" : ""
}
]}'
align='{align === "center" ? "center" : "right"}'
stackOnMobile="true"
fullWidthMobile="true"
size="lg"
color='{color}'
class="shrink-0"
>
<slot name="actions"></slot>
</HeroActions>
<slot name="visual"></slot>
</div>
<slot name="footer"></slot>
</div>
</Section>
}
}
@@ -0,0 +1,284 @@
component Card {
props {
@event click = function
@event action = function
@event navigate = function
@event dismiss = function
@event load = function
@event error = function
title = "Card title"
subtitle = ""
description = ""
header = ""
footer = ""
imageSrc = ""
imageAlt = ""
imagePosition = "top"
actionLabel = ""
actionHref = ""
headerActions = []
navigation = []
activeNav = ""
mobileNavigation = false
alertTitle = ""
alertDescription = ""
empty = false
emptyTitle = "No data to show"
emptyIcon = "icon-[lucide--inbox]"
items = []
size = "md"
color = "primary"
variant = "default"
layout = "vertical"
align = "left"
hover = "none"
scrollable = false
maxHeight = "18rem"
dismissible = false
ariaLabel = ""
class = ""
}
state dismissed = false
view {
<div
{...attrs}
data-ui-component="Card"
data-wrn-card
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
class='wire-next wire-next--card wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}'
>
{#if items.length > 0}
<div class="wire-next__card-group">
{#each items as item, itemIndex}
<article
class="wire-next__card-panel"
aria-label='{item.ariaLabel || item.title || item.label || "Card item"}'
data-index='{itemIndex}'
>
{#if item.imageSrc || item.image}
<div class="wire-next__card-media">
<img
src='{item.imageSrc || item.image}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if item.title || item.label}
<h3>{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wire-next__card-subtitle">{item.subtitle}</p>
{/if}
{#if item.description}
<p class="wire-next__card-description">{item.description}</p>
{/if}
{#if item.actionLabel || item.href}
<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 } }))'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</div>
</article>
{/each}
</div>
{:else}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if imageSrc && imagePosition === "overlay"}
<div class="wire-next__card-overlay">
<img
src='{imageSrc}'
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 } }))'
/>
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
</div>
{/if}
{#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if header || title || subtitle || headerActions.length > 0 || dismissible}
<header class="wire-next__card-header">
<div class="wire-next__card-heading">
{#if header}
<p class="wire-next__card-eyebrow">{header}</p>
{/if}
{#if title}
<h3>{title}</h3>
{/if}
{#if subtitle}
<p class="wire-next__card-subtitle">{subtitle}</p>
{/if}
</div>
{#if headerActions.length > 0 || dismissible}
<div class="wire-next__card-header-actions">
{#each headerActions as headerAction, actionIndex}
<button
type="button"
aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { action: headerAction, index: actionIndex } }))'
>
{#if headerAction.icon}
<span class='{headerAction.icon}' aria-hidden="true"></span>
{/if}
{#if headerAction.label && !headerAction.icon}
<span>{headerAction.label}</span>
{/if}
</button>
{/each}
{#if dismissible}
<button
type="button"
aria-label="Dismiss card"
@click='dismissed = true; event.currentTarget.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, detail: { title: title } }))'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</div>
{/if}
</header>
{/if}
{#if navigation.length > 0}
<nav aria-label='{ariaLabel || title || "Card navigation"}' class="wire-next__card-navigation">
{#if mobileNavigation}
<select
class="wire-next__card-navigation-select"
aria-label='{ariaLabel || title || "Card navigation"}'
@change='event.currentTarget.dispatchEvent(new CustomEvent("navigate", { bubbles: true, detail: { value: event.target.value } }))'
>
{#each navigation as navItem}
<option
value='{navItem.value || navItem.href || navItem.label}'
selected='{activeNav === navItem.value || activeNav === navItem.href}'
>
{navItem.label}
</option>
{/each}
</select>
{/if}
<div class="wire-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
{#each navigation as navItem, navIndex}
<button
type="button"
aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
@click='event.currentTarget.dispatchEvent(new CustomEvent("navigate", { bubbles: true, detail: { item: navItem, index: navIndex } }))'
>
{#if navItem.icon}
<span class='{navItem.icon}' aria-hidden="true"></span>
{/if}
<span>{navItem.label}</span>
{#if navItem.badge}
<span class="wire-next__card-tab-badge">{navItem.badge}</span>
{/if}
</button>
{/each}
</div>
</nav>
{/if}
{#if alertTitle || alertDescription}
<div class="wire-next__card-alert" role="status">
{#if alertTitle}
<strong>{alertTitle}</strong>
{/if}
{#if alertDescription}
<p>{alertDescription}</p>
{/if}
</div>
{/if}
<div
class="wire-next__card-body"
data-scrollable='{scrollable}'
style='max-height: {scrollable ? maxHeight : "none"};'
>
{#if empty}
<div class="wire-next__card-empty">
<span class='{emptyIcon}' aria-hidden="true"></span>
<p>{emptyTitle}</p>
</div>
{:else}
{#if description}
<p class="wire-next__card-description">{description}</p>
{/if}
<slot></slot>
{/if}
</div>
{#if footer || actionLabel}
<footer class="wire-next__card-footer">
{#if footer}
<span>{footer}</span>
{/if}
{#if actionLabel}
<a
href='{actionHref || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</footer>
{/if}
</div>
{#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
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 } }))'
/>
</div>
{/if}
</article>
{/if}
</div>
}
}
@@ -0,0 +1,22 @@
component Chart {
props {
@event select = function
@event dataPointClick = function
@event legendToggle = function
size = "default"
color = "primary"
title = "Chart"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chart wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,147 @@
component ChatBubble {
props {
@event action = function
@event messageClick = function
@event avatarClick = function
@event linkClick = function
size = "default"
color = "primary"
title = ""
description = ""
items = []
oneSided = false
showAvatars = false
showMetadata = false
ariaLabel = "Conversation"
variant = "default"
class = ""
}
functions {
function messageDirection(item) {
return item.direction === "outgoing" ? "outgoing" : "incoming"
}
function selectMessage(item, index) {
$emit("messageClick", {
item: item,
index: index,
direction: messageDirection(item)
})
}
function selectAvatar(sourceEvent, item, index) {
sourceEvent.stopPropagation()
$emit("avatarClick", {
item: item,
index: index,
direction: messageDirection(item)
})
}
function selectLink(sourceEvent, link, item, index) {
sourceEvent.stopPropagation()
$emit("linkClick", {
link: link,
item: item,
index: index
})
}
function selectAction(sourceEvent, item, index) {
sourceEvent.stopPropagation()
$emit("action", {
action: item.action || "retry",
item: item,
index: index
})
}
}
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
data-one-sided="{oneSided}"
role="log"
aria-label="{ariaLabel}"
aria-live="polite"
>
{#if title || description}
<header class="wire-next__chat-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
{#if items.length}
<div class="wire-next__chat-thread">
{#each items as item, index}
<article
class="wire-next__chat-message"
data-direction="{messageDirection(item)}"
aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
>
<div class="wire-next__chat-row">
{#if showAvatars && (item.avatarSrc || item.avatarFallback)}
<button
class="wire-next__chat-avatar"
type="button"
aria-label="{item.avatarLabel || item.author || 'Message author'}"
@click="selectAvatar(event, item, index)"
>
{#if item.avatarSrc}
<img src="{item.avatarSrc}" alt="{item.avatarAlt || ''}" />
{:else}
<span>{item.avatarFallback}</span>
{/if}
</button>
{/if}
<div
class="wire-next__chat-content"
role="button"
tabindex="0"
@click="selectMessage(item, index)"
@keydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMessage(item, index) }"
>
{#if item.title}<h4>{item.title}</h4>{/if}
{#if item.text}<p>{item.text}</p>{/if}
{#if item.bullets && item.bullets.length}
<ul>
{#each item.bullets as bullet}<li>{bullet}</li>{/each}
</ul>
{/if}
{#if item.links && item.links.length}
<nav aria-label="Message links">
{#each item.links as link}
<a
href="{link.href || '#'}"
@click="selectLink(event, link, item, index)"
>{link.label}</a>
{/each}
</nav>
{/if}
</div>
</div>
{#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
<footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
{#if item.statusIcon}<span class="{item.statusIcon}" aria-hidden="true"></span>{/if}
{#if item.status}<span>{item.status}</span>{/if}
{#if item.timestamp}<time datetime="{item.datetime || ''}">{item.timestamp}</time>{/if}
{#if item.actionLabel}
<button type="button" @click="selectAction(event, item, index)">
{item.actionLabel}
</button>
{/if}
</footer>
{/if}
</article>
{/each}
</div>
{/if}
<slot />
</section>
}
}
@@ -0,0 +1,173 @@
component Checkbox {
props {
@event input = function
@event change = function
@event focus = function
@event blur = function
@event invalid = function
size = "default"
color = "primary"
id = ""
name = ""
label = "Checkbox"
hiddenLabel = false
placeholder = ""
variant = "normal"
icon = ""
iconPosition = "start"
value = "on"
values = []
options = []
checked = false
indeterminate = false
orientation = "vertical"
card = false
rightAligned = false
list = false
helperText = ""
cornerHint = ""
error = ""
inline = false
readonly = false
disabled = false
required = false
class = ""
}
functions {
function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); $emit(nameEvent, { checked: sourceEvent.currentTarget.checked, value: sourceEvent.currentTarget.value, values: values, name: name, sourceEvent: sourceEvent }) }
function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--choice-field wire-next--checkbox-field {class}"
data-inline="{inline}"
data-invalid="{error ? 'true' : 'false'}"
data-orientation="{orientation}"
data-card="{card}"
data-list="{list}"
data-right-aligned="{rightAligned}"
>
<div
class="wire-next__field-heading"
>
{#if options.length || cornerHint}
<span
class="{hiddenLabel ? 'wire-next__sr-only' : ''}"
>
{label}
</span>
{#if cornerHint}
<span
class="wire-next__field-hint"
>
{cornerHint}
</span>
{/if}
{/if}
</div>
{#if options.length}
<div
class="wire-next__checkbox-group"
role="group"
aria-label="{hiddenLabel ? label : ''}"
aria-invalid="{error ? 'true' : 'false'}"
>
{#each options as option}
<label
class="wire-next__choice wire-next--checkbox"
for="{id || name}-{option.value}"
data-variant="{variant}"
data-disabled="{disabled || option.disabled}"
>
<input
id="{id || name}-{option.value}"
type="checkbox"
name="{name}"
value="{option.value}"
checked="{values.includes(option.value) || option.checked}"
disabled="{disabled || option.disabled}"
required="{required}"
readonly="{readonly}"
@click="preventReadonly(event)"
@input="emitField('input', event)"
@change="emitField('change', event)"
@focus="emitField('focus', event)"
@blur="emitField('blur', event)"
@invalid="emitField('invalid', event)"
/>
<span
class="wire-next__choice-copy"
>
<strong>{option.label}</strong>
{#if option.description}
<small>{option.description}</small>
{/if}
</span>
</label>
{/each}
</div>
{:else}
<div
class="wire-next__checkbox-content"
>
<label
class="wire-next__choice wire-next--checkbox"
for="{id || name}"
data-variant="{variant}"
data-icon-position="{iconPosition}"
>
<input
id="{id || name}"
type="checkbox"
name="{name}"
value="{value}"
checked="{checked}"
disabled="{disabled}"
required="{required}"
readonly="{readonly}"
aria-checked="{indeterminate ? 'mixed' : (checked ? 'true' : 'false')}"
aria-invalid="{error ? 'true' : 'false'}"
@click="preventReadonly(event)"
@input="emitField('input', event)"
@change="emitField('change', event)"
@focus="emitField('focus', event)"
@blur="emitField('blur', event)"
@invalid="emitField('invalid', event)"
/>
{#if icon}
<span
class="{icon}"
aria-hidden="true"
>
</span>
{/if}
<span
class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}"
>
{label}
</span>
<div
class="wire-next__checkbox-slot"
>
<slot></slot>
</div>
</label>
</div>
{/if}
{#if helperText}
<small
class="wire-next__field-help"
>
{helperText}
</small>
{/if}
<small
class="wire-next__field-error"
data-error="{name}"
>
{error}
</small>
</div>
}
}
@@ -0,0 +1,22 @@
component Clipboard {
props {
@event copy = function
@event success = function
@event error = function
size = "default"
color = "primary"
title = "Clipboard"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--clipboard wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,95 @@
component Collapse {
props {
@event toggle = function
@event open = function
@event close = function
size = "default"
color = "primary"
items = []
multiple = false
mode = "panel"
initialOpenIndexes = []
ariaLabel = "Collapsible content"
class = ""
}
state openIndexes = initialOpenIndexes
functions {
function isOpen(index) {
return openIndexes.includes(index)
}
function toggleItem(index, item, opening) {
if (item.disabled) {
return
}
opening = !isOpen(index)
if (opening) {
if (multiple) {
openIndexes = openIndexes.concat(index)
} else {
openIndexes = [index]
}
$emit("open", { index: index, item: item })
} else {
openIndexes = openIndexes.filter((openIndex) => openIndex !== index)
$emit("close", { index: index, item: item })
}
$emit("toggle", {
index: index,
item: item,
open: opening,
openIndexes: openIndexes
})
}
}
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--collapse {class}"
data-mode="{mode}"
aria-label="{ariaLabel}"
>
{#each items as item, index}
<article class="wire-next__collapse-item" data-open="{isOpen(index)}">
{#if mode === "inline" && item.preview}
<p class="wire-next__collapse-preview">{item.preview}</p>
{/if}
<button
class="wire-next__collapse-trigger"
type="button"
aria-expanded="{isOpen(index) ? 'true' : 'false'}"
aria-controls="{item.id || 'collapse-panel-' + index}"
disabled="{item.disabled}"
@click="toggleItem(index, item)"
>
<span>{isOpen(index) ? (item.closeLabel || "Read less") : item.label}</span>
<span class="icon-[lucide--chevron-down] wire-next__collapse-chevron" aria-hidden="true"></span>
</button>
<div
id="{item.id || 'collapse-panel-' + index}"
class="wire-next__collapse-panel"
data-open="{isOpen(index)}"
aria-hidden="{isOpen(index) ? 'false' : 'true'}"
inert="{!isOpen(index)}"
>
<div class="wire-next__collapse-content">
{#if item.title}<h4>{item.title}</h4>{/if}
{#if item.content}<p>{item.content}</p>{/if}
{#if item.links && item.links.length}
<nav aria-label="Related links">
{#each item.links as link}<a href="{link.href || '#'}">{link.label}</a>{/each}
</nav>
{/if}
</div>
</div>
</article>
{/each}
<slot />
</section>
}
}
@@ -0,0 +1,38 @@
component ColorPicker {
props {
@event input = function
@event change = function
@event focus = function
@event blur = function
size = "default"
color = "primary"
id = ""
name = ""
label = "Color"
hiddenLabel = false
placeholder = ""
value = "#2563eb"
icon = ""
iconPosition = "start"
helperText = ""
cornerHint = ""
error = ""
inline = false
variant = "normal"
readonly = false
disabled = false
required = false
class = ""
}
functions {
function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); $emit(nameEvent, { value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--color-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__color-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<input id="{id || name}" type="color" name="{name}" value="{value}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" /><output>{value}</output></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
@@ -0,0 +1,35 @@
component Columns {
props {
size = "default"
color = "primary"
columns = 2
gap = "md"
maxWidth = "xl"
class = ""
}
view {
<div
data-ui-component="Columns"
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"'
>
<slot></slot>
</div>
}
}
@@ -0,0 +1,386 @@
component ComboBox {
props {
size = "default"
color = "primary"
label = "ComboBox"
name = ""
value = ""
options = []
groups = []
placeholder = "Search or select an option"
searchPlaceholder = "Start typing…"
clearable = true
allowCustomValue = false
disabled = false
required = false
invalid = false
validationMessage = ""
helpText = ""
loading = false
loadingLabel = "Loading suggestions…"
emptyLabel = "No matching options"
clearLabel = "Clear value"
toggleLabel = "Toggle suggestions"
searchMode = "contains"
searchFields = "label,description"
minSearchLength = 0
searchResultLimit = 0
optionTemplate = "default"
defaultOpen = false
closeOnSelect = true
fixed = false
placement = "bottom"
autocomplete = "off"
remote = false
remoteUrl = ""
remoteQueryParam = "q"
remoteDebounce = 250
remoteAutoLoad = true
infinite = false
hasMore = false
page = 1
loadMoreLabel = "Load more"
class = ""
@event search = function
@event select = function
@event change = function
@event clear = function
@event open = function
@event close = function
@event load = function
@event error = function
}
state open = defaultOpen
state query = ""
state selectedValue = value
state activeIndex = -1
functions {
function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
function matches(option) {
if (!query || query.length < minSearchLength) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
function matchingOptions(list) {
return list.filter((option) => matches(option))
}
function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, searchResultLimit)
}
return matchingOptions(list)
}
function flatVisibleOptions() {
return visibleOptions(allOptions())
}
function isSelected(option) {
return selectedValue === option.value
}
function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
function selectedText() {
return selectedOptions().length ? selectedOptions()[0].label : selectedValue
}
function inputText() {
return query || selectedText()
}
function shouldShowDropdown() {
if (!open) {
return false
}
if (loading) {
return true
}
if (remote && query.length !== 0 && query.length < minSearchLength) {
return true
}
if (flatVisibleOptions().length) {
return true
}
return infinite && hasMore
}
function canSelect(option) {
return !option.disabled
}
function chooseOption(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = option.label
activeIndex = optionIndex(option)
if (closeOnSelect) {
open = false
}
}
function updateQuery(event) {
query = event.target.value
selectedValue = allowCustomValue ? event.target.value : ""
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
function clearValue(event) {
if (event) {
event.stopPropagation()
}
query = ""
selectedValue = ""
activeIndex = -1
open = false
}
function openDropdown() {
if (!disabled) {
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
}
function closeDropdown() {
open = false
}
function toggle() {
if (open) {
closeDropdown()
} else {
openDropdown()
}
}
function setValue(nextValue) {
selectedValue = nextValue
query = ""
}
function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
openDropdown()
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
openDropdown()
moveActive(-1)
} else if (event.key === "Enter" && open && activeIndex >= 0) {
event.preventDefault()
chooseOption(flatVisibleOptions()[activeIndex])
} else if (event.key === "Escape") {
closeDropdown()
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-select wire-next--combobox {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
data-wrn-combobox
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { closeDropdown() }"
>
<label id="{name}-label" for="{name}-input">{label}</label>
<div class="wire-next__select-control wire-next__combobox-control">
<span class="wire-next__search-icon icon-[lucide--search]" aria-hidden="true"></span>
<input
{...attrs}
id="{name}-input"
class="wire-next__select-trigger wire-next__combobox-input"
type="text"
role="combobox"
name="{allowCustomValue ? name : ''}"
value="{inputText()}"
placeholder="{placeholder}"
autocomplete="{autocomplete}"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
required="{required && allowCustomValue}"
@focus="openDropdown()"
@click="openDropdown()"
@input="updateQuery(event)"
@keydown="handleKeydown(event)"
/>
<button
type="button"
class="wire-next__clear-select"
data-show="clearable && inputText() && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearValue(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
<button
type="button"
class="wire-next__combobox-toggle"
aria-label="{toggleLabel}"
tabindex="-1"
disabled="{disabled}"
@click="toggle()"
>
<span class="wire-next__select-chevron icon-[lucide--chevrons-up-down]" aria-hidden="true"></span>
</button>
</div>
<div
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
data-show="shouldShowDropdown()"
style="{shouldShowDropdown() ? '' : 'display: none'}"
>
<div
class="wire-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<div class="wire-next__select-message" data-show="loading" role="status">
<i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
role="listbox"
>
{#each groups as group}
{#if visibleOptions(group.options || []).length}
<div class="wire-next__option-group" role="group" aria-label="{group.label}">
<div class="wire-next__group-label">{group.label}</div>
{#each group.options || [] as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
<button
type="button"
class="wire-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
{#if !allowCustomValue}
<input
type="hidden"
name="{name}"
value="{selectedValue}"
required="{required}"
data-error="{name}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{/if}
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
}
}
@@ -0,0 +1,21 @@
component Confetti {
props {
@event start = function
@event complete = function
size = "default"
color = "primary"
title = "Confetti"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--confetti wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,46 @@
component Container {
props {
size = "default"
color = "primary"
columns = 2
gap = "md"
maxWidth = "xl"
class = ""
}
view {
<div
data-ui-component="Container"
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"'
>
<slot></slot>
</div>
}
}
@@ -0,0 +1,22 @@
component ContextMenu {
props {
@event open = function
@event close = function
@event select = function
size = "default"
color = "primary"
title = "Context Menu"
description = ""
open = false
placement = "bottom"
closeLabel = "Close"
class = ""
}
view {
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--context-menu wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
{#if description}<p>{description}</p>{/if}
<slot />
</div>
}
}
@@ -0,0 +1,23 @@
component CopyMarkup {
props {
@event copy = function
@event success = function
@event error = function
size = "default"
color = "primary"
label = "Copy Markup"
name = ""
value = ""
placeholder = ""
type = "text"
min = ""
max = ""
step = ""
disabled = false
required = false
class = ""
}
view {
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--copy-markup {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
}
}
@@ -0,0 +1,14 @@
component CustomScrollbar {
props {
@event scroll = function
size = "default"
color = "primary"
columns = 2
gap = "md"
maxWidth = "xl"
class = ""
}
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>
}
}
@@ -0,0 +1,21 @@
component DataMap {
props {
@event select = function
@event change = function
size = "default"
color = "primary"
title = "Data Map"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--data-map wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,19 @@
component DataTable {
props {
@event sort = function
@event select = function
@event change = function
@event rowClick = function
@event pageChange = function
size = "default"
color = "primary"
caption = "Data Table"
columns = []
rows = []
striped = true
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--table {class}"><table><caption>{caption}</caption><thead><tr>{#each columns as column}<th>{column.label}</th>{/each}</tr></thead><tbody>{#each rows as row}<tr>{#each columns as column}<td>{row[column.key]}</td>{/each}</tr>{/each}</tbody></table><slot /></div>
}
}
@@ -0,0 +1,59 @@
component DatePicker {
props {
@event input = function
@event change = function
@event open = function
@event close = function
@event focus = function
@event blur = function
@event invalid = function
size = "default"
color = "primary"
label = "Date Picker"
id = ""
name = ""
value = ""
placeholder = ""
type = "date"
locale = "en-US"
firstDayOfWeek = 0
months = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
years = [2024, 2025, 2026, 2027, 2028, 2029, 2030]
min = ""
max = ""
step = ""
helperText = ""
cornerHint = ""
error = ""
variant = "normal"
inline = false
readonly = false
disabled = false
required = false
class = ""
}
state currentValue = value
state selectedYear = value ? value.split("-")[0] : ""
state selectedMonth = value ? value.split("-")[1] : ""
state selectedDay = value ? value.split("-")[2] : ""
state expanded = false
functions {
function openCalendar(sourceEvent) { expanded = true; $emit("open", { value: currentValue, name: name, sourceEvent: sourceEvent }) }
function updateDate(part, nextValue, sourceEvent, nextDate, detail) { if (part === "year") { selectedYear = String(nextValue) } if (part === "month") { selectedMonth = String(nextValue).padStart(2, "0") } if (part === "day") { selectedDay = String(nextValue).padStart(2, "0") } if (!selectedYear || !selectedMonth || !selectedDay) { return } nextDate = selectedYear + "-" + selectedMonth + "-" + selectedDay; if ((min && nextDate < min) || (max && nextDate > max)) { return } currentValue = nextDate; detail = { value: currentValue, year: selectedYear, month: selectedMonth, day: selectedDay, name: name, sourceEvent: sourceEvent }; $emit("input", detail); $emit("change", detail) }
function finishDate(sourceEvent) { if (!currentValue) { return } expanded = false; $emit("close", { value: currentValue, name: name, sourceEvent: sourceEvent }) }
function selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--date-picker"); input = root.querySelector(".wire-next__picker-value"); nextDate = input.getAttribute("value").slice(0, 5) + root.querySelector("select").getAttribute("value") + "-" + sourceEvent.currentTarget.dataset.value; currentValue = nextDate; input.setAttribute("value", nextDate); trigger = root.querySelector(".wire-next__date-trigger span"); if (trigger) { trigger.replaceChildren(nextDate) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: nextDate, name: name, sourceEvent: sourceEvent }; $emit("input", detail); $emit("change", detail) }
function clearDate(sourceEvent, detail) { if (disabled || readonly) { return } currentValue = ""; detail = { value: currentValue, name: name, sourceEvent: sourceEvent }; $emit("input", detail); $emit("change", detail) }
function handleFocus(sourceEvent) { $emit("focus", { value: currentValue, name: name, sourceEvent: sourceEvent }) }
function handleBlur(sourceEvent) { $emit("blur", { value: currentValue, name: name, sourceEvent: sourceEvent }) }
function handleInvalid(sourceEvent) { $emit("invalid", { name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
<div class="wire-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__picker-control"><input id="{id || name}" class="wire-next__sr-only wire-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wire-next__date-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select date"}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wire-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
<div class="wire-next__calendar" role="dialog" aria-label="{label}"><div class="wire-next__date-selectors"><label>Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label>Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)">{#each years as year}<option value="{year}" selected="{String(year) === selectedYear}">{year}</option>{/each}</select></label></div><div class="wire-next__calendar-grid">{#each days as day}<button type="button" data-value="{String(day).padStart(2, '0')}" aria-pressed="{String(day).padStart(2, '0') === selectedDay}" @click="selectDay(event)">{day}</button>{/each}</div><button type="button" class="wire-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
@@ -0,0 +1,31 @@
component DeviceFrame {
props {
@event change = function
@event rotate = function
size = "default"
color = "primary"
title = "Device Frame"
description = ""
items = []
variant = "default"
device = "phone"
orientation = "portrait"
src = ""
srcdoc = ""
frameTitle = "Device preview"
showToolbar = true
allow = ""
class = ""
}
state currentOrientation = orientation
functions {
function setDevice(nextDevice, sourceEvent) { $emit("change", { device: nextDevice, orientation: currentOrientation, sourceEvent: sourceEvent }) }
function rotateFrame(sourceEvent) { currentOrientation = currentOrientation === "portrait" ? "landscape" : "portrait"; $emit("rotate", { device: device, orientation: currentOrientation, sourceEvent: sourceEvent }); $emit("change", { device: device, orientation: currentOrientation, sourceEvent: sourceEvent }) }
}
view {
<section {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--device-frame wire-next--variant-{variant} {class}" data-device="{device}" data-orientation="{currentOrientation}">
<header><div>{#if title}<strong>{title}</strong>{/if}{#if description}<p>{description}</p>{/if}</div>{#if showToolbar}<div class="wire-next__device-actions" role="toolbar" aria-label="Preview device">{#each items as item}<button type="button" aria-pressed="{item.value === device}" @click="setDevice(item.value, event)">{item.label}</button>{/each}<button type="button" aria-label="Rotate preview" @click="rotateFrame(event)">↻</button></div>{/if}</header>
<div class="wire-next__device-shell">{#if src || srcdoc}<iframe title="{frameTitle}" src="{src}" srcdoc="{srcdoc}" allow="{allow}"></iframe>{:else}<div class="wire-next__device-content"><slot /></div>{/if}</div>
</section>
}
}
@@ -0,0 +1,69 @@
component Divider {
props {
size = "default"
color = "primary"
label = ""
orientation = "horizontal"
class = ""
}
view {
<div
{...attrs}
data-ui-component="Divider"
data-size='{size}'
data-color='{color}'
data-orientation='{orientation}'
class='w-full {class}'
class:h-full='orientation === "vertical"'
class:w-auto='orientation === "vertical"'
>
{#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}
</div>
}
}
@@ -0,0 +1,25 @@
component DragAndDrop {
props {
@event dragStart = function
@event dragEnd = function
@event dragEnter = function
@event dragLeave = function
@event drop = function
@event change = function
size = "default"
color = "primary"
title = "Drag And Drop"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--drag-and-drop wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,21 @@
component Drawer {
props {
@event open = function
@event close = function
size = "default"
color = "primary"
title = "Drawer"
description = ""
open = false
placement = "bottom"
closeLabel = "Close"
class = ""
}
view {
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--drawer wire-next--placement-{placement} {class}" role="dialog" aria-modal="true" aria-label="{title}">
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
{#if description}<p>{description}</p>{/if}
<slot />
</div>
}
}
@@ -0,0 +1,53 @@
component Dropdown {
props {
@event toggle = function
@event open = function
@event close = function
@event select = function
size = "default"
color = "primary"
label = "Open menu"
items = []
placement = "end"
class = ""
}
functions {
function toggleMenu(event) {
$emit("toggle", { open: event.currentTarget.open })
$emit(event.currentTarget.open ? "open" : "close", { source: "trigger" })
}
function selectItem(item) {
$emit("select", { item: item, value: item.value || "" })
}
}
view {
<details class="wire-dropdown wire-dropdown--{placement} wire-next--color-{color} wire-next--size-{size} {class}" data-wrn-dropdown @toggle="toggleMenu($event)">
<summary aria-label="{label}">
<slot name="trigger" />
<span class="wire-dropdown__chevron icon-[lucide--chevron-down]" aria-hidden="true"></span>
</summary>
<div class="wire-dropdown__panel" role="menu" aria-label="{label}">
<slot name="header" />
{#each items as item}
{#if item.type === "divider"}
<hr />
{:else}
{#if item.type === "header"}
<span class="wire-dropdown__heading">{item.label}</span>
{:else}
<a class="wire-dropdown__item {item.danger ? 'wire-dropdown__item--danger' : ''}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" role="menuitem" @click="selectItem(item)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
{#if item.external}<span class="icon-[lucide--arrow-up-right]" aria-hidden="true"></span>{/if}
</a>
{/if}
{/if}
{/each}
<slot name="footer" />
</div>
</details>
}
}
@@ -0,0 +1,113 @@
component FeatureCard {
props {
icon = ""
title = "Feature"
description = ""
href = ""
actionLabel = "Learn more"
actionIcon = ""
badge = ""
badgeColor = "primary"
size = "default"
color = "primary"
variant = "default"
hover = "lift"
align = "left"
disabled = false
class = ""
}
view {
<article
data-ui-component="FeatureCard"
aria-disabled='{disabled}'
class='group relative flex h-full min-w-0 flex-col overflow-hidden rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] shadow-sm transition duration-200 {class}'
class:p-4='size === "sm"'
class:p-6='size === "default" || size === "md"'
class:p-8='size === "lg"'
class:bg-[var(--wire-color-primary-soft)]='variant === "soft" && color === "primary"'
class:bg-[var(--wire-color-success-soft)]='variant === "soft" && color === "success"'
class:bg-[var(--wire-color-warning-soft)]='variant === "soft" && color === "warning"'
class:bg-[var(--wire-color-danger-soft)]='variant === "soft" && color === "danger"'
class:bg-transparent='variant === "ghost"'
class:border-transparent='variant === "ghost"'
class:shadow-none='variant === "ghost" || variant === "outline"'
class:hover:-translate-y-1='hover === "lift" && !disabled'
class:hover:shadow-xl='(hover === "lift" || hover === "shadow") && !disabled'
class:hover:border-[var(--wire-color-primary)]='(hover === "lift" || hover === "border") && !disabled'
class:text-center='align === "center"'
class:items-center='align === "center"'
class:text-right='align === "right"'
class:items-end='align === "right"'
class:opacity-60='disabled'
class:pointer-events-none='disabled'
>
<div class="flex w-full items-start justify-between gap-4">
<div
class="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]"
class:bg-[var(--wire-color-success-soft)]='color === "success"'
class:text-[var(--wire-color-success)]='color === "success"'
class:bg-[var(--wire-color-warning-soft)]='color === "warning"'
class:text-[var(--wire-color-warning-text)]='color === "warning"'
class:bg-[var(--wire-color-danger-soft)]='color === "danger"'
class:text-[var(--wire-color-danger)]='color === "danger"'
class:bg-[var(--wire-color-info-soft)]='color === "info"'
class:text-[var(--wire-color-info)]='color === "info"'
class:size-10='size === "sm"'
class:size-14='size === "lg"'
>
<slot name="icon"></slot>
{#if icon}
<span class='{icon + " size-6"}' aria-hidden="true"></span>
{/if}
</div>
{#if badge}
<Badge
label='{badge}'
size="sm"
color='{badgeColor}'
variant="soft"
/>
{/if}
</div>
<h3 class="mt-5 text-lg font-bold leading-tight text-[var(--wire-color-text)]" class:text-xl='size === "lg"'>
{title}
</h3>
{#if description}
<p class="mt-3 flex-1 text-sm leading-6 text-[var(--wire-color-text-muted)]" class:text-base='size === "lg"'>
{description}
</p>
{/if}
<slot></slot>
<div class="mt-6 flex w-full items-center justify-between gap-3">
<slot name="footer"></slot>
{#if href && actionLabel}
<TextLink
label='{actionLabel}'
href='{href}'
icon='{actionIcon}'
iconPosition="start"
showArrow="true"
size="sm"
color='{color}'
class="ml-auto"
/>
{/if}
</div>
{#if href}
<a
href='{href}'
aria-label='{title}'
class="absolute inset-0 z-10 rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
></a>
{/if}
</article>
}
}
@@ -0,0 +1,53 @@
component FeatureGrid {
props {
columns = 3
tabletColumns = 2
mobileColumns = 1
gap = "md"
minItemWidth = ""
equalHeight = true
align = "stretch"
maxWidth = "full"
class = ""
}
view {
<div
data-ui-component="FeatureGrid"
data-columns='{columns}'
data-tablet-columns='{tabletColumns}'
data-mobile-columns='{mobileColumns}'
class='grid w-full {class}'
class:wire-feature-grid-autofit='minItemWidth !== ""'
class:grid-cols-1='mobileColumns === 1'
class:grid-cols-2='mobileColumns === 2'
class:sm:grid-cols-1='tabletColumns === 1'
class:sm:grid-cols-2='tabletColumns === 2'
class:sm:grid-cols-3='tabletColumns === 3'
class:lg: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-3='gap === "sm"'
class:gap-5='gap === "md"'
class:gap-8='gap === "lg"'
class:gap-10='gap === "xl"'
class:items-stretch='equalHeight || align === "stretch"'
class:items-start='align === "start"'
class:items-center='align === "center"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-none='maxWidth === "full"'
style='--wire-feature-grid-min: {minItemWidth || "16rem"};'
>
<slot></slot>
</div>
}
style {
.wire-feature-grid-autofit {
grid-template-columns: repeat(auto-fit, minmax(var(--wire-feature-grid-min), 1fr));
}
}
}
@@ -0,0 +1,78 @@
component FeatureIconCard {
props {
icon = "icon-[lucide--sparkles]"
iconSize = "md"
iconVariant = "soft"
title = "Feature"
description = ""
href = ""
actionLabel = "Explore"
badge = ""
size = "default"
color = "primary"
variant = "default"
align = "left"
hover = "lift"
class = ""
}
view {
<article
data-ui-component="FeatureIconCard"
class='group relative flex h-full flex-col rounded-3xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] p-6 shadow-sm transition duration-200 {class}'
class:p-5='size === "sm"'
class:p-8='size === "lg"'
class:bg-[var(--wire-color-surface-soft)]='variant === "soft"'
class:bg-transparent='variant === "ghost"'
class:border-transparent='variant === "ghost"'
class:shadow-none='variant === "ghost"'
class:hover:-translate-y-1='hover === "lift"'
class:hover:shadow-xl='hover === "lift" || hover === "shadow"'
class:hover:border-[var(--wire-color-primary)]='hover === "lift" || hover === "border"'
class:items-center='align === "center"'
class:text-center='align === "center"'
>
<div
class="relative flex size-14 items-center justify-center rounded-2xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)] ring-1 ring-inset ring-[var(--wire-color-primary-muted)]"
class:size-12='iconSize === "sm"'
class:size-16='iconSize === "lg"'
class:bg-[var(--wire-color-primary)]='iconVariant === "solid" && color === "primary"'
class:text-[var(--wire-color-on-primary)]='iconVariant === "solid" && color === "primary"'
class:bg-[var(--wire-color-success-soft)]='color === "success" && iconVariant !== "solid"'
class:text-[var(--wire-color-success)]='color === "success" && iconVariant !== "solid"'
class:bg-[var(--wire-color-warning-soft)]='color === "warning" && iconVariant !== "solid"'
class:text-[var(--wire-color-warning-text)]='color === "warning" && iconVariant !== "solid"'
class:bg-[var(--wire-color-danger-soft)]='color === "danger" && iconVariant !== "solid"'
class:text-[var(--wire-color-danger)]='color === "danger" && iconVariant !== "solid"'
>
<span class='{icon + " size-7"}' aria-hidden="true"></span>
{#if badge}
<span class="absolute -right-2 -top-2 rounded-full bg-[var(--wire-color-surface-raised)] px-2 py-1 text-[10px] font-bold uppercase tracking-wide text-[var(--wire-color-primary)] shadow-sm ring-1 ring-[var(--wire-color-border)]">
{badge}
</span>
{/if}
</div>
<h3 class="mt-6 text-xl font-bold tracking-tight text-[var(--wire-color-text)]">{title}</h3>
{#if description}
<p class="mt-3 flex-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
<slot></slot>
{#if href}
<TextLink
label='{actionLabel}'
href='{href}'
color='{color}'
size="sm"
class="mt-6"
/>
{/if}
<slot name="footer"></slot>
</article>
}
}
@@ -0,0 +1,52 @@
component FileInput {
props {
@event input = function
@event change = function
@event focus = function
@event blur = function
@event select = function
@event clear = function
@event invalid = function
size = "default"
color = "primary"
id = ""
name = ""
label = "File"
hiddenLabel = false
placeholder = "Choose a file"
value = ""
icon = "icon-[lucide--upload]"
iconPosition = "start"
accept = ""
multiple = false
helperText = ""
cornerHint = ""
error = ""
inline = false
variant = "normal"
readonly = false
disabled = false
required = false
class = ""
}
state selectedName = ""
functions {
function handleChange(sourceEvent) {
sourceEvent.stopPropagation()
selectedName = sourceEvent.currentTarget.files && sourceEvent.currentTarget.files.length ? sourceEvent.currentTarget.files[0].name : ""
$emit("input", { files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
$emit("change", { files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
if (selectedName) $emit("select", { files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
else $emit("clear", { name: name, sourceEvent: sourceEvent })
}
function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); $emit(nameEvent, { name: name, sourceEvent: sourceEvent }) }
function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--file-input {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__file-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<span>{selectedName || value || placeholder}</span><input id="{id || name}" type="file" name="{name}" accept="{accept}" multiple="{multiple}" disabled="{disabled}" required="{required}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="handleChange(event)" @change="handleChange(event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" /></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
@@ -0,0 +1,26 @@
component FileUpload {
props {
@event select = function
@event upload = function
@event progress = function
@event success = function
@event error = function
@event cancel = function
@event remove = function
size = "default"
color = "primary"
title = "File Upload"
description = ""
items = []
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,37 @@
component FileUploadProgress {
props {
@event cancel = function
@event retry = function
@event complete = function
size = "default"
color = "primary"
label = "Progress"
value = 50
max = 100
showValue = true
fileName = ""
fileSize = ""
uploadedSize = ""
status = "uploading"
cancelLabel = "Cancel upload"
retryLabel = "Retry upload"
class = ""
}
state currentValue = value
state currentStatus = status
functions {
function percent() { if (!Number(max)) { return 0 } return Math.max(0, Math.min(100, Math.round(Number(currentValue) / Number(max) * 100))) }
function detail(sourceEvent) { return { value: currentValue, max: max, percent: percent(), status: currentStatus, fileName: fileName, sourceEvent: sourceEvent } }
function cancelUpload(sourceEvent) { currentStatus = "cancelled"; $emit("cancel", detail(sourceEvent)) }
function retryUpload(sourceEvent) { currentValue = 0; currentStatus = "uploading"; $emit("retry", detail(sourceEvent)) }
function completeUpload(sourceEvent) { currentValue = max; currentStatus = "complete"; $emit("complete", detail(sourceEvent)) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload-progress {class}" data-status="{currentStatus}" aria-live="polite">
<div class="wire-next__row"><span>{#if fileName}<strong>{fileName}</strong><small>{#if uploadedSize || fileSize}{uploadedSize}{#if uploadedSize && fileSize} of {/if}{fileSize}{:else}{label}{/if}</small>{:else}<strong>{label}</strong>{/if}</span>{#if showValue}<strong>{percent()}%</strong>{/if}</div>
<progress value="{currentValue}" max="{max}" aria-label="{label}"></progress>
<div class="wire-next__upload-status"><span class="wire-next__upload-status-icon" aria-hidden="true"></span><span>{#if currentStatus === "uploading"}Uploading securely…{:else if currentStatus === "complete"}Upload complete{:else if currentStatus === "error"}Upload failed{:else}Upload cancelled{/if}</span></div>
<div class="wire-next__upload-actions">{#if currentStatus === "uploading"}<button type="button" @click="cancelUpload(event)">{cancelLabel}</button><button type="button" @click="completeUpload(event)">Complete</button>{/if}{#if currentStatus === "error" || currentStatus === "cancelled"}<button type="button" @click="retryUpload(event)">{retryLabel}</button>{/if}</div>
</div>
}
}
@@ -0,0 +1,144 @@
component Footer {
props {
@event select = function
@event action = function
size = "default"
color = "primary"
label = "Footer navigation"
items = []
columns = 3
maxWidth = "compact"
copyright = ""
class = ""
}
view {
<footer
{...attrs}
data-ui-component="Footer"
data-size='{size}'
data-color='{color}'
class='wire-footer border-t border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] {class}'
>
<slot name="pre-footer"></slot>
<div
class="wire-footer__inner mx-auto w-full px-4 py-12 sm:px-6 lg:px-8"
class:max-w-5xl='maxWidth === "compact"'
class:max-w-7xl='maxWidth === "wide" || maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "full"'
class:py-8='size === "compact"'
class:py-16='size === "spacious"'
>
<nav aria-label='{label}'>
<div class='wire-footer__columns wire-footer--columns-{columns}'>
{#each items as item, itemIndex}
{#if item.type === "header"}
<section class="wire-footer__column" aria-labelledby='footer-heading-{itemIndex}'>
<h2 id='footer-heading-{itemIndex}' class="wire-footer__heading">
{item.label || item.title}
</h2>
{#if item.description}
<p class="wire-footer__description">{item.description}</p>
{/if}
{#if (item.items || item.links || []).length > 0}
<ul class="wire-footer__column-links">
{#each item.items || item.links || [] as child, childIndex}
<li>
<a
href='{child.href || "#"}'
target='{child.target || ""}'
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
class="wire-footer__link"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } })); child.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } }))'
>
{#if child.icon}
<span class='{child.icon}' aria-hidden="true"></span>
{/if}
<span>{child.label || child.title}</span>
{#if child.external}
<span class="icon-[lucide--external-link]" aria-hidden="true"></span>
{/if}
</a>
</li>
{/each}
</ul>
{/if}
</section>
{:else if item.type === "link" || item.href}
<section class="wire-footer__column">
<ul class="wire-footer__column-links">
<li>
<a
href='{item.href || "#"}'
target='{item.target || ""}'
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
class="wire-footer__link"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } })); item.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
>
{#if item.icon}
<span class='{item.icon}' aria-hidden="true"></span>
{/if}
<span>{item.label || item.title}</span>
{#if item.external}
<span class="icon-[lucide--external-link]" aria-hidden="true"></span>
{/if}
</a>
</li>
</ul>
</section>
{:else}
<section class="wire-footer__column">
{#if item.title || item.label}
<h2 class="wire-footer__heading">{item.title || item.label}</h2>
{/if}
{#if item.description}
<p class="wire-footer__description">{item.description}</p>
{/if}
<ul class="wire-footer__column-links">
{#each item.items || item.links || [] as child, childIndex}
<li>
<a
href='{child.href || "#"}'
target='{child.target || ""}'
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
class="wire-footer__link"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } })); child.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } }))'
>
{#if child.icon}
<span class='{child.icon}' aria-hidden="true"></span>
{/if}
<span>{child.label || child.title}</span>
{#if child.external}
<span class="icon-[lucide--external-link]" aria-hidden="true"></span>
{/if}
</a>
</li>
{/each}
</ul>
</section>
{/if}
{/each}
</div>
</nav>
<div class="wire-footer__copyright">
<div class="wire-footer__copyright-left">
<slot name="copyright-left"></slot>
{#if copyright}
<p>{copyright}</p>
{/if}
</div>
<div class="wire-footer__copyright-right">
<slot name="copyright-right"></slot>
</div>
</div>
</div>
<slot name="post-footer"></slot>
</footer>
}
}
@@ -0,0 +1,40 @@
component Grid {
props {
size = "default"
color = "primary"
columns = 2
gap = "md"
maxWidth = "xl"
class = ""
}
view {
<div
data-ui-component="Grid"
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"'
>
<slot></slot>
</div>
}
}
@@ -0,0 +1,149 @@
component Hero {
props {
eyebrow = ""
title = "Build something remarkable"
highlight = ""
description = ""
align = "left"
size = "default"
color = "primary"
variant = "default"
primaryLabel = ""
primaryHref = ""
primaryIcon = ""
secondaryLabel = ""
secondaryHref = ""
secondaryIcon = ""
tertiaryLabel = ""
tertiaryHref = ""
badges = []
trustItems = []
maxWidth = "xl"
class = ""
}
view {
<Section
spacing='{size === "compact" ? "md" : (size === "large" ? "xl" : "lg")}'
maxWidth='{maxWidth}'
variant='{variant === "solid" ? "solid" : (variant === "soft" ? "soft" : "default")}'
color='{color}'
class='overflow-hidden {class}'
>
<div
data-ui-component="Hero"
class="relative isolate"
class:text-center='align === "center"'
>
{#if variant === "gradient"}
<div
class="pointer-events-none absolute inset-0 -z-10 rounded-[2rem] opacity-80"
style="background: radial-gradient(circle at 15% 20%, var(--wire-color-primary-muted), transparent 35%), radial-gradient(circle at 85% 75%, var(--wire-color-secondary-muted), transparent 36%);"
aria-hidden="true"
></div>
{/if}
<div
class="max-w-4xl"
class:mx-auto='align === "center"'
class:ml-auto='align === "right"'
>
<slot name="eyebrow"></slot>
{#if eyebrow}
<div
class="mb-6 inline-flex items-center gap-2 rounded-full border border-[var(--wire-color-primary-muted)] bg-[var(--wire-color-primary-soft)] px-4 py-2 text-sm font-semibold text-[var(--wire-color-primary)]"
>
<span class="size-2 rounded-full bg-[var(--wire-color-primary)]" aria-hidden="true"></span>
<span>{eyebrow}</span>
</div>
{/if}
<h1
class="font-bold leading-[1.06] tracking-[-0.04em]"
class:text-4xl='size === "compact"'
class:sm:text-5xl='size === "compact"'
class:text-5xl='size === "default"'
class:sm:text-6xl='size === "default"'
class:lg:text-7xl='size === "default" || size === "large"'
class:text-[var(--wire-color-text)]='variant !== "solid"'
>
<span>{title}</span>
{#if highlight}
<span class="block bg-gradient-to-r from-[var(--wire-color-primary)] to-[var(--wire-color-secondary)] bg-clip-text text-transparent">
{highlight}
</span>
{/if}
</h1>
{#if description}
<p
class="mt-6 max-w-3xl text-base leading-8 text-[var(--wire-color-text-muted)] sm:text-lg"
class:mx-auto='align === "center"'
class:ml-auto='align === "right"'
class:text-[var(--wire-color-on-primary)]='variant === "solid"'
class:opacity-85='variant === "solid"'
>
{description}
</p>
{/if}
{#if badges.length > 0}
<div class="mt-6 flex flex-wrap gap-2" class:justify-center='align === "center"' class:justify-end='align === "right"'>
{#each badges as badge}
<Badge
label='{badge.label || badge.title || badge}'
icon='{badge.icon || ""}'
color='{badge.color || color}'
variant='{badge.variant || "soft"}'
size='{badge.size || "sm"}'
/>
{/each}
</div>
{/if}
<div class="mt-8">
<HeroActions
actions='{[
{ label: primaryLabel, href: primaryHref, icon: primaryIcon, variant: "default", color: color },
{ label: secondaryLabel, href: secondaryHref, icon: secondaryIcon, variant: "outline", color: color },
{ label: tertiaryLabel, href: tertiaryHref, variant: "ghost", color: color }
]}'
align='{align}'
stackOnMobile="true"
fullWidthMobile="true"
size='{size === "compact" ? "default" : "lg"}'
color='{color}'
>
<slot name="actions"></slot>
</HeroActions>
</div>
{#if trustItems.length > 0}
<div
class="mt-8 flex flex-wrap items-center gap-x-6 gap-y-3 text-sm text-[var(--wire-color-text-muted)]"
class:justify-center='align === "center"'
class:justify-end='align === "right"'
>
{#each trustItems as item}
<span class="inline-flex items-center gap-2">
<span class='{(item.icon || "icon-[lucide--check-circle-2]") + " size-4 text-[var(--wire-color-primary)]"}' aria-hidden="true"></span>
<span>{item.label || item.title || item}</span>
</span>
{/each}
</div>
{/if}
<slot name="trust"></slot>
<slot></slot>
</div>
<div class="mt-10">
<slot name="visual"></slot>
</div>
<slot name="footer"></slot>
</div>
</Section>
}
}
@@ -0,0 +1,52 @@
component HeroActions {
props {
actions = []
align = "left"
orientation = "horizontal"
stackOnMobile = true
fullWidthMobile = true
size = "default"
color = "primary"
class = ""
}
view {
<div
data-ui-component="HeroActions"
role="group"
aria-label="Page actions"
class='flex flex-wrap items-center gap-3 {class}'
class:flex-col='orientation === "vertical" || stackOnMobile'
class:sm:flex-row='orientation === "horizontal" && stackOnMobile'
class:justify-center='align === "center"'
class:justify-end='align === "right"'
class:w-full='fullWidthMobile'
class:sm:w-auto='fullWidthMobile'
>
{#each actions as action, index}
{#if action.label}
<Button
label='{action.label}'
href='{action.href || ""}'
target='{action.target || ""}'
rel='{action.rel || ""}'
type='{action.type || "button"}'
variant='{action.variant || (index === 0 ? "default" : "outline")}'
color='{action.color || color}'
size='{action.size || size}'
disabled='{action.disabled || false}'
loading='{action.loading || false}'
icon='{action.icon || ""}'
iconPosition='{action.iconPosition || "start"}'
ariaLabel='{action.ariaLabel || action.label}'
fullWidth='{fullWidthMobile}'
controlClass='{action.controlClass || ""}'
class='{"sm:w-auto " + (action.class || "")}'
/>
{/if}
{/each}
<slot></slot>
</div>
}
}
@@ -0,0 +1,47 @@
component Image {
props {
size = "default"
color = "primary"
src = ""
alt = ""
width = ""
height = ""
loading = "lazy"
rounded = false
class = ""
}
view {
<figure
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='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}
<slot></slot>
</figure>
}
}
@@ -0,0 +1,92 @@
component Input {
props {
@event input = function
@event change = function
@event focus = function
@event blur = function
@event invalid = function
@event keydown = function
@event keyup = function
size = "default"
color = "primary"
id = ""
name = ""
label = "Input"
hiddenLabel = false
placeholder = ""
value = ""
type = "text"
variant = "normal"
icon = ""
iconPosition = "start"
helperText = ""
cornerHint = ""
error = ""
inline = false
readonly = false
disabled = false
required = false
autocomplete = ""
inputmode = ""
minlength = ""
maxlength = ""
pattern = ""
min = ""
max = ""
step = ""
class = ""
}
functions {
function detail(sourceEvent) {
return { value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }
}
function handleInput(sourceEvent) { sourceEvent.stopPropagation(); $emit("input", detail(sourceEvent)) }
function handleChange(sourceEvent) { sourceEvent.stopPropagation(); $emit("change", detail(sourceEvent)) }
function handleFocus(sourceEvent) { sourceEvent.stopPropagation(); $emit("focus", detail(sourceEvent)) }
function handleBlur(sourceEvent) { sourceEvent.stopPropagation(); $emit("blur", detail(sourceEvent)) }
function handleInvalid(sourceEvent) { sourceEvent.stopPropagation(); $emit("invalid", { value: sourceEvent.currentTarget.value, name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
function handleKeydown(sourceEvent) { sourceEvent.stopPropagation(); $emit("keydown", { key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
function handleKeyup(sourceEvent) { sourceEvent.stopPropagation(); $emit("keyup", { key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--input {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading">
<label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>
{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}
</div>
<div class="wire-next__field-control" data-icon-position="{iconPosition}">
{#if icon}<span class="{icon} wire-next__field-icon" aria-hidden="true"></span>{/if}
<input
id="{id || name}"
type="{type}"
name="{name}"
value="{value}"
placeholder="{variant === 'floating' ? ' ' : placeholder}"
readonly="{readonly}"
disabled="{disabled}"
required="{required}"
autocomplete="{autocomplete}"
inputmode="{inputmode}"
minlength="{minlength}"
maxlength="{maxlength}"
pattern="{pattern}"
min="{min}"
max="{max}"
step="{step}"
aria-invalid="{error ? 'true' : 'false'}"
aria-describedby="{error ? (id || name) + '-error' : (helperText ? (id || name) + '-help' : '')}"
@input="handleInput(event)"
@change="handleChange(event)"
@focus="handleFocus(event)"
@blur="handleBlur(event)"
@invalid="handleInvalid(event)"
@keydown="handleKeydown(event)"
@keyup="handleKeyup(event)"
/>
{#if variant === "floating"}<span class="wire-next__field-floating-label">{label}</span>{/if}
</div>
{#if helperText}<small id="{(id || name) + '-help'}" class="wire-next__field-help">{helperText}</small>{/if}
<small id="{(id || name) + '-error'}" class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
@@ -0,0 +1,51 @@
component InputGroup {
props {
@event input = function
@event change = function
@event focus = function
@event blur = function
@event submit = function
@event action = function
size = "default"
color = "primary"
id = ""
name = ""
label = "Input group"
hiddenLabel = false
value = ""
placeholder = ""
type = "text"
startText = ""
endText = ""
icon = ""
iconPosition = "start"
actionLabel = ""
helperText = ""
cornerHint = ""
error = ""
inline = false
variant = "normal"
readonly = false
disabled = false
required = false
class = ""
}
functions {
function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); $emit(nameEvent, { value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
function handleKeydown(sourceEvent) { if (sourceEvent.key === "Enter") { sourceEvent.stopPropagation(); $emit("submit", { value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) } }
function handleAction(sourceEvent) { $emit("action", { name: name, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--input-group {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__input-group-control">
{#if startText}<span class="wire-next__input-addon">{startText}</span>{/if}
{#if icon}<span class="{icon} wire-next__field-icon" data-position="{iconPosition}" aria-hidden="true"></span>{/if}
<input id="{id || name}" type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" readonly="{readonly}" disabled="{disabled}" required="{required}" aria-invalid="{error ? 'true' : 'false'}" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @keydown="handleKeydown(event)" />
{#if endText}<span class="wire-next__input-addon">{endText}</span>{/if}
{#if actionLabel}<button type="button" disabled="{disabled}" @click="handleAction(event)">{actionLabel}</button>{/if}
</div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
@@ -0,0 +1,623 @@
component InputNumber {
props {
size = "default"
color = "primary"
variant = "default"
class = ""
id = ""
name = "quantity"
value = 0
min = ""
max = ""
step = 1
precision = "auto"
label = ""
description = ""
helpText = ""
error = ""
invalid = false
prefix = ""
suffix = ""
placeholder = ""
autocomplete = "off"
inputMode = "decimal"
ariaLabel = ""
required = false
disabled = false
inputDisabled = false
buttonsDisabled = false
readonly = false
allowInput = true
keyboard = true
wheel = false
clamp = true
fullWidth = false
showButtons = true
showValidationMessage = true
decrementLabel = "Decrease value"
incrementLabel = "Increase value"
controlsLabel = "Quantity controls"
requiredMessage = "A value is required."
minMessage = "Value is below the minimum."
maxMessage = "Value is above the maximum."
@event input = function
@event change = function
@event increment = function
@event decrement = function
}
state currentValue = value
state committedValue = value
functions {
function inputId() {
if (id !== "") {
return id
}
if (name !== "") {
return name
}
return "input-number"
}
function descriptionId() {
return inputId() + "-description"
}
function messageId() {
return inputId() + "-message"
}
function componentColor() {
if (color === "secondary") {
return "var(--wire-color-secondary)"
}
if (color === "success") {
return "var(--wire-color-success)"
}
if (color === "warning") {
return "var(--wire-color-warning)"
}
if (color === "danger") {
return "var(--wire-color-danger)"
}
if (color === "info") {
return "var(--wire-color-info)"
}
return "var(--wire-color-primary)"
}
function hasMin() {
return min !== "" && min !== null && min !== undefined
}
function hasMax() {
return max !== "" && max !== null && max !== undefined
}
function isBlank() {
return currentValue === "" || currentValue === null || currentValue === undefined
}
function normalizedStep() {
return Number(step) > 0 ? Number(step) : 1
}
function inferredPrecision() {
if (precision !== "auto" && precision !== "") {
return Math.max(0, Number(precision) || 0)
}
if (String(normalizedStep()).includes(".")) {
return String(normalizedStep()).split(".")[1].length
}
return 0
}
function precisionFactor() {
return Math.pow(10, inferredPrecision())
}
function roundValue(nextValue) {
return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor()
}
function clampValue(nextValue) {
nextValue = Number(nextValue)
if (hasMin() && nextValue < Number(min)) {
nextValue = Number(min)
}
if (hasMax() && nextValue > Number(max)) {
nextValue = Number(max)
}
return roundValue(nextValue)
}
function isBelowMin() {
return !isBlank() && hasMin() && Number(currentValue) < Number(min)
}
function isAboveMax() {
return !isBlank() && hasMax() && Number(currentValue) > Number(max)
}
function isInvalid() {
return (
invalid ||
error !== "" ||
(required && isBlank()) ||
isBelowMin() ||
isAboveMax()
)
}
function validationMessage() {
if (error !== "") {
return error
}
if (required && isBlank()) {
return requiredMessage
}
if (isBelowMin()) {
return minMessage
}
if (isAboveMax()) {
return maxMessage
}
return ""
}
function hasMessage() {
return (
helpText !== "" ||
(showValidationMessage && isInvalid() && validationMessage() !== "")
)
}
function describedBy() {
if (description !== "" && hasMessage()) {
return descriptionId() + " " + messageId()
}
if (description !== "") {
return descriptionId()
}
if (hasMessage()) {
return messageId()
}
return ""
}
function decrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMin() && !isBlank() && Number(currentValue) <= Number(min))
)
}
function incrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMax() && !isBlank() && Number(currentValue) >= Number(max))
)
}
function dispatchInputNumberEvent(
sourceEvent,
eventName,
action,
previousValue,
root,
customEvent
) {
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, {
component: "InputNumber",
name: name,
value: currentValue,
previousValue: previousValue,
action: action,
min: hasMin() ? Number(min) : null,
max: hasMax() ? Number(max) : null,
step: normalizedStep(),
valid: !isInvalid()
})
root.dispatchEvent(customEvent)
}
function applyControlValue(nextValue, action, sourceEvent, previousValue) {
previousValue = currentValue
currentValue = clampValue(nextValue)
committedValue = currentValue
dispatchInputNumberEvent(
sourceEvent,
"input",
action,
previousValue
)
dispatchInputNumberEvent(
sourceEvent,
"change",
action,
previousValue
)
dispatchInputNumberEvent(
sourceEvent,
action,
action,
previousValue
)
}
function incrementValue(sourceEvent, nextValue) {
if (incrementDisabled()) {
return
}
if (isBlank()) {
nextValue = hasMin() ? Number(min) : normalizedStep()
} else {
nextValue =
Number(currentValue) +
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
}
applyControlValue(nextValue, "increment", sourceEvent)
}
function decrementValue(sourceEvent, nextValue) {
if (decrementDisabled()) {
return
}
if (isBlank()) {
nextValue = hasMax() ? Number(max) : -normalizedStep()
} else {
nextValue =
Number(currentValue) -
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
}
applyControlValue(nextValue, "decrement", sourceEvent)
}
function handleInput(sourceEvent, previousValue, nextValue) {
sourceEvent.stopPropagation()
previousValue = currentValue
nextValue = sourceEvent.target.value
if (nextValue === "") {
currentValue = ""
} else if (!Number.isNaN(Number(nextValue))) {
currentValue = roundValue(Number(nextValue))
}
dispatchInputNumberEvent(
sourceEvent,
"input",
"input",
previousValue
)
}
function handleChange(sourceEvent, previousValue) {
sourceEvent.stopPropagation()
previousValue = committedValue
if (!isBlank()) {
currentValue = clamp
? clampValue(currentValue)
: roundValue(currentValue)
}
committedValue = currentValue
dispatchInputNumberEvent(
sourceEvent,
"change",
"change",
previousValue
)
}
function handleKeydown(sourceEvent) {
if (
!keyboard ||
disabled ||
readonly ||
inputDisabled ||
!allowInput
) {
return
}
if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
incrementValue(sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
decrementValue(sourceEvent)
} else if (sourceEvent.key === "Home" && hasMin()) {
sourceEvent.preventDefault()
applyControlValue(Number(min), "decrement", sourceEvent)
} else if (sourceEvent.key === "End" && hasMax()) {
sourceEvent.preventDefault()
applyControlValue(Number(max), "increment", sourceEvent)
}
}
function handleWheel(sourceEvent) {
if (
!wheel ||
disabled ||
readonly ||
inputDisabled ||
!allowInput
) {
return
}
sourceEvent.preventDefault()
if (sourceEvent.deltaY < 0) {
incrementValue(sourceEvent)
} else if (sourceEvent.deltaY > 0) {
decrementValue(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-wrn-input-number
data-variant='{variant}'
data-size='{size}'
data-color='{color}'
data-value='{currentValue}'
data-invalid='{isInvalid() ? "true" : "false"}'
data-disabled='{disabled ? "true" : "false"}'
style='--input-number-accent: {componentColor()};'
class='relative flex flex-col gap-1.5 text-[var(--wire-color-text)] {fullWidth ? "w-full" : variant === "compact" ? "w-fit max-w-full" : "w-full max-w-sm"} {disabled ? "opacity-60" : ""} {class}'
>
{#if label !== "" && variant !== "labeled" && variant !== "seat"}
<label
for='{inputId()}'
class='inline-flex items-center gap-1 text-sm font-semibold leading-5 text-[var(--wire-color-text)]'
>
<span>{label}</span>
{#if required}
<span
aria-hidden="true"
class='text-[var(--wire-color-danger)]'
>*</span>
{/if}
</label>
{/if}
{#if description !== "" && variant !== "labeled" && variant !== "seat"}
<p
id='{descriptionId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
>
{description}
</p>
{/if}
<div
class='group flex min-w-0 overflow-hidden border bg-[var(--wire-color-surface)] text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] transition-[border-color,box-shadow,background-color] duration-[var(--wire-motion-base)] ease-[var(--wire-ease-standard)] focus-within:border-[var(--input-number-accent)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--input-number-accent)_18%,transparent)] {variant === "compact" ? "rounded-full" : "rounded-[var(--wire-radius-sm)]"} {size === "xs" ? "min-h-8 text-xs" : size === "sm" ? "min-h-9 text-sm" : size === "lg" ? "min-h-12 text-base" : size === "xl" ? "min-h-14 text-lg" : "min-h-10 text-sm"} {isInvalid() ? "border-[var(--wire-color-danger)] focus-within:border-[var(--wire-color-danger)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--wire-color-danger)_18%,transparent)]" : "border-[var(--wire-color-border)]"} {disabled ? "cursor-not-allowed bg-[var(--wire-color-surface-2)]" : ""}'
>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='inline-flex shrink-0 items-center justify-center border-r border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
></span>
</button>
{/if}
<div
class='flex min-w-0 flex-1 {variant === "labeled" ? "flex-col items-stretch justify-center gap-0.5" : variant === "seat" ? "items-center justify-between gap-3" : "items-center"} {size === "xs" ? "px-2" : size === "sm" ? "px-2.5" : size === "lg" ? "px-4" : size === "xl" ? "px-5" : "px-3"}'
>
{#if variant === "labeled" || variant === "seat"}
<div class='min-w-0 flex-1'>
{#if label !== ""}
<label
for='{inputId()}'
class='block truncate font-semibold leading-4 text-[var(--wire-color-text)] {variant === "labeled" ? "text-xs font-medium text-[var(--wire-color-muted)]" : "text-sm"}'
>
{label}
{#if required}
<span
aria-hidden="true"
class='ml-0.5 text-[var(--wire-color-danger)]'
>*</span>
{/if}
</label>
{/if}
{#if description !== ""}
<span
id='{descriptionId()}'
class='block truncate text-xs leading-4 text-[var(--wire-color-muted)]'
>
{description}
</span>
{/if}
</div>
{/if}
<div
class='flex min-w-0 items-center {variant === "seat" ? "w-auto shrink-0" : "w-full"}'
>
{#if prefix !== ""}
<span
aria-hidden="true"
class='shrink-0 pr-1.5 text-[var(--wire-color-muted)]'
>
{prefix}
</span>
{/if}
<input
id='{inputId()}'
name='{name}'
type="number"
value='{currentValue}'
min='{min}'
max='{max}'
step='{normalizedStep()}'
placeholder='{placeholder}'
autocomplete='{autocomplete}'
inputmode='{inputMode}'
aria-label='{ariaLabel !== "" ? ariaLabel : label !== "" ? label : name}'
aria-describedby='{describedBy()}'
aria-invalid='{isInvalid() ? "true" : "false"}'
aria-required='{required ? "true" : "false"}'
aria-disabled='{disabled || inputDisabled ? "true" : "false"}'
required='{required}'
disabled='{disabled}'
readonly='{readonly || inputDisabled || !allowInput}'
tabindex='{inputDisabled ? "-1" : "0"}'
@input='handleInput(event)'
@change='handleChange(event)'
@keydown='handleKeydown(event)'
@wheel='handleWheel(event)'
class='min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-medium leading-none text-[var(--wire-color-text)] outline-none placeholder:text-[var(--wire-color-muted)] read-only:cursor-default disabled:cursor-not-allowed [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none {variant === "horizontal" || variant === "compact" || variant === "seat" ? "text-center" : "text-left"} {variant === "seat" ? "w-10 flex-none" : "w-full"}'
/>
{#if suffix !== ""}
<span
aria-hidden="true"
class='shrink-0 pl-1.5 text-[var(--wire-color-muted)]'
>
{suffix}
</span>
{/if}
</div>
</div>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='inline-flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
></span>
</button>
{:else}
{#if showButtons}
<div
role="group"
aria-label='{controlsLabel}'
class='flex shrink-0 border-l border-[var(--wire-color-border)] {variant === "vertical" ? "flex-col" : "flex-row"}'
>
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1 border-b border-[var(--wire-color-border)]" : "border-r border-[var(--wire-color-border)]"} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
></span>
</button>
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1" : ""} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
></span>
</button>
</div>
{/if}
{/if}
</div>
{#if showValidationMessage && isInvalid() && validationMessage() !== ""}
<p
id='{messageId()}'
role="alert"
aria-live="polite"
class='m-0 flex items-center gap-1.5 text-xs leading-5 text-[var(--wire-color-danger)]'
>
<span
aria-hidden="true"
class='icon-[lucide--circle-alert] size-3.5 shrink-0'
></span>
<span>{validationMessage()}</span>
</p>
{/if}
{#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
<p
id='{messageId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
>
{helpText}
</p>
{/if}
</div>
}
}
@@ -0,0 +1,9 @@
component Kbd {
props {
size = "default"
color = "primary"
label = "⌘ K"
class = ""
}
view { <kbd class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--kbd {class}"><slot>{label}</slot></kbd> }
}
@@ -0,0 +1,16 @@
component LayoutSplitter {
props {
@event resizeStart = function
@event resize = function
@event resizeEnd = function
size = "default"
color = "primary"
columns = 2
gap = "md"
maxWidth = "xl"
class = ""
}
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>
}
}

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