release: WRNexusJS 0.5.12
This commit is contained in:
@@ -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,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
@@ -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"
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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,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,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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,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,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",
|
||||
|
||||
@@ -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,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,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,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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,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,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
@@ -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
@@ -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
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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
Reference in New Issue
Block a user