release: WRNexusJS 0.5.12

This commit is contained in:
2026-07-30 18:59:01 +05:30
parent f37303b22d
commit d64e899993
462 changed files with 75432 additions and 2570 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.5.11",
"version": "0.5.12",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
"version": "0.5.11",
"version": "0.5.12",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+16
View File
@@ -1137,6 +1137,22 @@ const MIGRATIONS: Migration[] = [
// Gateway, helper, validation, and UI improvements are package-managed.
},
},
{
version: "0.5.11",
id: "latest-release-after-upgrade",
description: "Latest New Packages and release ready packages",
apply() {
// Gateway, helper, validation, and UI improvements are package-managed.
},
},
{
version: "0.5.12",
id: "latest-release-after-upgrade",
description: "Latest New Packages and release ready packages",
apply() {
// Gateway, helper, validation, and UI improvements are package-managed.
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.5.11",
"version": "0.5.12",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+171 -9
View File
@@ -1,6 +1,6 @@
# WRNexus UI component reference
This reference is generated from the 90 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
This reference is generated from the 108 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
## Advanced-forms
@@ -56,7 +56,7 @@ Theme-aware, responsive search box component.
- Mount: `data-component="SearchBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Events: `input`, `change`, `search`, `submit`, `clear`
- Events: None
### StrongPassword
@@ -222,6 +222,15 @@ Theme-aware, responsive file upload progress component.
- Slots: None
- Events: `cancel`, `retry`, `complete`
### HeroActions
Responsive action group for hero and call-to-action sections.
- Mount: `data-component="HeroActions"`
- Props: `actions: string = []`, `align: string = "left"`, `orientation: string = "horizontal"`, `stackOnMobile: boolean = true`, `fullWidthMobile: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
- Slots: `default`
- Events: None
### LegendIndicator
Theme-aware, responsive legend indicator component.
@@ -238,7 +247,7 @@ Theme-aware, responsive list component.
- Mount: `data-component="List"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `select`, `click`
- Events: None
### ListGroup
@@ -256,7 +265,7 @@ Theme-aware, responsive marquee component.
- Mount: `data-component="Marquee"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `pause`, `resume`
- Events: None
### Progress
@@ -310,7 +319,7 @@ Theme-aware, responsive timeline component.
- Mount: `data-component="Timeline"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `select`
- Events: None
### Toast
@@ -332,6 +341,78 @@ Theme-aware, responsive tree view component.
## Blocks
### AnnouncementBar
Accessible public announcement, emergency, maintenance, or status bar.
- Mount: `data-component="AnnouncementBar"`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Slots: None
- Events: None
### CTASection
Reusable closing call-to-action section with primary and secondary actions.
- Mount: `data-component="CTASection"`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`, `actions`, `visual`, `footer`
- Events: None
### FeatureCard
Reusable linked feature or service card with icon, badge, description, and action.
- Mount: `data-component="FeatureCard"`
- Props: `icon: string = ""`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Learn more"`, `actionIcon: string = ""`, `badge: string = ""`, `badgeColor: string = "primary"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `disabled: boolean = false`, `class: string = ""`
- Slots: `icon`, `default`, `footer`
- Events: None
### FeatureIconCard
Feature card with a prominent themed icon treatment.
- Mount: `data-component="FeatureIconCard"`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
- Slots: `default`, `footer`
- Events: None
### Hero
Reusable public-page hero with actions, trust items, badges, and visual slots.
- Mount: `data-component="Hero"`
- Props: `eyebrow: string = ""`, `title: string = "Build something remarkable"`, `highlight: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `tertiaryLabel: string = ""`, `tertiaryHref: string = ""`, `badges: string = []`, `trustItems: string = []`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `eyebrow`, `actions`, `trust`, `default`, `visual`, `footer`
- Events: None
### MarketingSectionHeader
Marketing section heading with an optional linked action.
- Mount: `data-component="MarketingSectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "split"`, `size: string = "default"`, `color: string = "primary"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `actionExternal: boolean = false`, `class: string = ""`
- Slots: `icon`, `actions`, `default`
- Events: None
### MetricCard
Single statistic or operational metric with icon, trend, and optional action.
- Mount: `data-component="MetricCard"`
- Props: `label: string = "Metric"`, `value: string = "0"`, `description: string = ""`, `icon: string = ""`, `prefix: string = ""`, `suffix: string = ""`, `trend: string = ""`, `trendLabel: string = ""`, `trendDirection: string = "neutral"`, `href: string = ""`, `actionLabel: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `class: string = ""`
- Slots: None
- Events: None
### PageHeader
Public and application page header with breadcrumbs, metadata, and actions.
- Mount: `data-component="PageHeader"`
- Props: `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `align: string = "left"`, `size: string = "default"`, `compact: boolean = false`, `showBreadcrumbs: boolean = false`, `breadcrumbs: string = []`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `meta`, `actions`, `default`
- Events: None
### PortalDashboard
Responsive portal dashboard with metrics, actions, tasks, and updates.
@@ -341,6 +422,24 @@ Responsive portal dashboard with metrics, actions, tasks, and updates.
- Slots: `hero-action`
- Events: `action`, `navigate`
### SplitHero
Two-column hero with configurable content and visual placement.
- Mount: `data-component="SplitHero"`
- Props: `eyebrow: string = ""`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `trustItems: string = []`, `class: string = ""`
- Slots: `actions`, `trust`, `default`, `visual`
- Events: None
### StatsBar
Compact responsive statistics strip.
- Mount: `data-component="StatsBar"`
- Props: `items: string = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: None
- Events: None
## Forms
### AuthForm
@@ -541,7 +640,7 @@ Theme-aware, responsive map component.
- Mount: `data-component="Map"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: `ready`, `move`, `zoom`, `markerClick`
- Events: None
### ToastNotifications
@@ -608,6 +707,15 @@ Theme-aware, responsive divider component.
- Slots: None
- Events: None
### FeatureGrid
Responsive equal-height grid for feature and service cards.
- Mount: `data-component="FeatureGrid"`
- Props: `color: string = "primary"`, `size: string = "default"`, `columns: number = 3`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `minItemWidth: string = ""`, `equalHeight: boolean = true`, `align: string = "stretch"`, `maxWidth: string = "full"`, `class: string = ""`
- Slots: `default`
- Events: None
### Grid
Theme-aware, responsive grid component.
@@ -624,7 +732,7 @@ Theme-aware, responsive image component.
- Mount: `data-component="Image"`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `rounded: boolean = false`, `class: string = ""`
- Slots: `default`
- Events: `load`, `error`
- Events: None
### Kbd
@@ -651,6 +759,51 @@ Theme-aware, responsive link component.
- Mount: `data-component="Link"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `class: string = ""`
- Slots: `default`
- Events: None
### MetricGrid
Responsive collection of metric cards.
- Mount: `data-component="MetricGrid"`
- Props: `items: string = []`, `columns: number = 4`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `equalHeight: boolean = true`, `dividers: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Events: None
### PublicPageShell
Consistent structural wrapper for public-facing pages.
- Mount: `data-component="PublicPageShell"`
- Props: `maxWidth: string = "full"`, `fullWidth: boolean = true`, `headerOffset: string = "none"`, `background: string = "default"`, `overflow: string = "clip"`, `minHeight: string = "screen"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `before`, `default`, `after`
- Events: None
### Section
Theme-aware page section with consistent spacing, width, background, and border controls.
- Mount: `data-component="Section"`
- Props: `id: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `spacing: string = "lg"`, `maxWidth: string = "xl"`, `fullWidth: boolean = false`, `borderTop: boolean = false`, `borderBottom: boolean = false`, `class: string = ""`
- Slots: `default`
- Events: None
### SectionHeader
Reusable accessible section heading with eyebrow, title, description, icon, and actions.
- Mount: `data-component="SectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `headingLevel: number = 2`, `maxWidth: string = "3xl"`, `class: string = ""`
- Slots: `icon`, `default`, `actions`
- Events: None
### TextLink
Styled inline action link with icons, arrows, external state, and disabled handling.
- Mount: `data-component="TextLink"`
- Props: `label: string = "Learn more"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `showArrow: boolean = true`, `underline: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `disabled: boolean = false`, `class: string = ""`
- Slots: None
- Events: `click`, `focus`, `blur`
### Typography
@@ -664,6 +817,15 @@ Theme-aware, responsive typography component.
## Navigation
### BackToTop
Accessible scroll-to-top control with visibility threshold.
- Mount: `data-component="BackToTop"`
- Props: `threshold: number = 500`, `label: string = "Back to top"`, `ariaLabel: string = "Scroll back to top"`, `icon: string = "icon-[lucide--arrow-up]"`, `position: string = "right"`, `offset: string = "md"`, `behavior: string = "smooth"`, `showProgress: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `class: string = ""`
- Slots: None
- Events: None
### Breadcrumb
Theme-aware, responsive breadcrumb component.
@@ -679,7 +841,7 @@ Responsive application footer with structured links and pre/post content slots.
- Mount: `data-component="Footer"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: string = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
- Slots: `pre-footer`, `copyright-left`, `copyright-right`, `post-footer`
- Events: `select`, `action`
### MegaMenu
@@ -761,7 +923,7 @@ Theme-aware, responsive tabs component.
- Mount: `data-component="Tabs"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Tabs"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Events: `change`, `select`
- Events: None
## Overlays
+412 -322
View File
@@ -1,66 +1,47 @@
{
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
"components": [
{
"name": "Container",
"category": "layout",
"purpose": "Theme-aware, responsive container component."
},
{
"name": "Columns",
"category": "layout",
"purpose": "Theme-aware, responsive columns component."
},
{
"name": "Grid",
"category": "layout",
"purpose": "Theme-aware, responsive grid component."
},
{
"name": "LayoutSplitter",
"category": "layout",
"purpose": "Theme-aware, responsive layout splitter component."
},
{
"name": "Typography",
"category": "layout",
"purpose": "Theme-aware, responsive typography component."
},
{
"name": "Image",
"category": "layout",
"purpose": "Theme-aware, responsive image component."
},
{
"name": "Link",
"category": "layout",
"purpose": "Theme-aware, responsive link component."
},
{
"name": "Divider",
"category": "layout",
"purpose": "Theme-aware, responsive divider component."
},
{
"name": "Kbd",
"category": "layout",
"purpose": "Theme-aware, responsive kbd component."
},
{
"name": "CustomScrollbar",
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component."
},
{
"name": "Accordion",
"category": "base",
"purpose": "Theme-aware, responsive accordion component."
},
{
"name": "AdvancedDatePicker",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced date picker component."
},
{
"name": "AdvancedRangeSlider",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced range slider component."
},
{
"name": "AdvancedSelect",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive advanced select component.",
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
},
{
"name": "Alert",
"category": "base",
"purpose": "Theme-aware, responsive alert component."
},
{
"name": "AnnouncementBar",
"category": "blocks",
"purpose": "Accessible public announcement, emergency, maintenance, or status bar."
},
{
"name": "AuthForm",
"category": "forms",
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA."
},
{
"name": "AuthSplitLayout",
"category": "layout",
"purpose": "Responsive 50/50 authentication layout with content and form regions."
},
{
"name": "Avatar",
"category": "base",
@@ -71,6 +52,11 @@
"category": "base",
"purpose": "Theme-aware, responsive avatar group component."
},
{
"name": "BackToTop",
"category": "navigation",
"purpose": "Accessible scroll-to-top control with visibility threshold."
},
{
"name": "Badge",
"category": "base",
@@ -81,6 +67,11 @@
"category": "base",
"purpose": "Theme-aware, responsive blockquote component."
},
{
"name": "Breadcrumb",
"category": "navigation",
"purpose": "Theme-aware, responsive breadcrumb component."
},
{
"name": "Button",
"category": "base",
@@ -96,165 +87,20 @@
"category": "base",
"purpose": "Theme-aware, responsive card component."
},
{
"name": "ChatBubble",
"category": "base",
"purpose": "Theme-aware, responsive chat bubble component."
},
{
"name": "Carousel",
"category": "base",
"purpose": "Theme-aware, responsive carousel component."
},
{
"name": "Collapse",
"name": "Chart",
"category": "integrations",
"purpose": "Theme-aware, responsive chart component."
},
{
"name": "ChatBubble",
"category": "base",
"purpose": "Theme-aware, responsive collapse component."
},
{
"name": "DatePicker",
"category": "base",
"purpose": "Theme-aware, responsive date picker component."
},
{
"name": "DeviceFrame",
"category": "base",
"purpose": "Theme-aware, responsive device frame component."
},
{
"name": "List",
"category": "base",
"purpose": "Theme-aware, responsive list component."
},
{
"name": "ListGroup",
"category": "base",
"purpose": "Theme-aware, responsive list group component."
},
{
"name": "LegendIndicator",
"category": "base",
"purpose": "Theme-aware, responsive legend indicator component."
},
{
"name": "Progress",
"category": "base",
"purpose": "Theme-aware, responsive progress component."
},
{
"name": "FileUploadProgress",
"category": "base",
"purpose": "Theme-aware, responsive file upload progress component."
},
{
"name": "Rating",
"category": "base",
"purpose": "Theme-aware, responsive rating component."
},
{
"name": "Skeleton",
"category": "base",
"purpose": "Theme-aware, responsive skeleton component."
},
{
"name": "Spinner",
"category": "base",
"purpose": "Theme-aware, responsive spinner component."
},
{
"name": "StyledIcon",
"category": "base",
"purpose": "Theme-aware, responsive styled icon component."
},
{
"name": "Toast",
"category": "base",
"purpose": "Theme-aware, responsive toast component."
},
{
"name": "Timeline",
"category": "base",
"purpose": "Theme-aware, responsive timeline component."
},
{
"name": "TreeView",
"category": "base",
"purpose": "Theme-aware, responsive tree view component."
},
{
"name": "Marquee",
"category": "base",
"purpose": "Theme-aware, responsive marquee component."
},
{
"name": "Navbar",
"category": "navigation",
"purpose": "Theme-aware, responsive navbar component."
},
{
"name": "Footer",
"category": "navigation",
"purpose": "Responsive application footer with structured links and pre/post content slots."
},
{
"name": "MegaMenu",
"category": "navigation",
"purpose": "Theme-aware, responsive mega menu component."
},
{
"name": "Nav",
"category": "navigation",
"purpose": "Theme-aware, responsive nav component."
},
{
"name": "Tabs",
"category": "navigation",
"purpose": "Theme-aware, responsive tabs component."
},
{
"name": "Sidebar",
"category": "navigation",
"purpose": "Theme-aware, responsive sidebar component."
},
{
"name": "Scrollspy",
"category": "navigation",
"purpose": "Theme-aware, responsive scrollspy component."
},
{
"name": "Breadcrumb",
"category": "navigation",
"purpose": "Theme-aware, responsive breadcrumb component."
},
{
"name": "Pagination",
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component."
},
{
"name": "Stepper",
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component."
},
{
"name": "Input",
"category": "forms",
"purpose": "Theme-aware, responsive input component."
},
{
"name": "InputGroup",
"category": "forms",
"purpose": "Theme-aware, responsive input group component."
},
{
"name": "Textarea",
"category": "forms",
"purpose": "Theme-aware, responsive textarea component."
},
{
"name": "FileInput",
"category": "forms",
"purpose": "Theme-aware, responsive file input component."
"purpose": "Theme-aware, responsive chat bubble component."
},
{
"name": "Checkbox",
@@ -262,24 +108,14 @@
"purpose": "Theme-aware, responsive checkbox component."
},
{
"name": "Radio",
"category": "forms",
"purpose": "Theme-aware, responsive radio component."
"name": "Clipboard",
"category": "integrations",
"purpose": "Theme-aware, responsive clipboard component."
},
{
"name": "Switch",
"category": "forms",
"purpose": "Theme-aware, responsive switch component."
},
{
"name": "Select",
"category": "forms",
"purpose": "Theme-aware, responsive select component."
},
{
"name": "RangeSlider",
"category": "forms",
"purpose": "Theme-aware, responsive range slider component."
"name": "Collapse",
"category": "base",
"purpose": "Theme-aware, responsive collapse component."
},
{
"name": "ColorPicker",
@@ -287,15 +123,9 @@
"purpose": "Theme-aware, responsive color picker component."
},
{
"name": "TimePicker",
"category": "forms",
"purpose": "Theme-aware, responsive time picker component."
},
{
"name": "AdvancedSelect",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive advanced select component.",
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
"name": "Columns",
"category": "layout",
"purpose": "Theme-aware, responsive columns component."
},
{
"name": "ComboBox",
@@ -304,46 +134,14 @@
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
},
{
"name": "SearchBox",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive search box component."
"name": "Confetti",
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component."
},
{
"name": "InputNumber",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive input number component."
},
{
"name": "StrongPassword",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive strong password component."
},
{
"name": "TogglePassword",
"category": "advanced-forms",
"purpose": "Accessible password field with optional show and hide controls.",
"events": ["input", "change", "toggle"]
},
{
"name": "ToggleCount",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive toggle count component."
},
{
"name": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component."
},
{
"name": "PinInput",
"category": "advanced-forms",
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support.",
"events": ["input", "change", "complete", "paste", "clear", "error"]
},
{
"name": "Dropdown",
"category": "overlays",
"purpose": "Theme-aware, responsive dropdown component."
"name": "Container",
"category": "layout",
"purpose": "Theme-aware, responsive container component."
},
{
"name": "ContextMenu",
@@ -351,54 +149,19 @@
"purpose": "Theme-aware, responsive context menu component."
},
{
"name": "Modal",
"category": "overlays",
"purpose": "Theme-aware, responsive modal component."
"name": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component."
},
{
"name": "Drawer",
"category": "overlays",
"purpose": "Theme-aware, responsive drawer component."
"name": "CTASection",
"category": "blocks",
"purpose": "Reusable closing call-to-action section with primary and secondary actions."
},
{
"name": "Popover",
"category": "overlays",
"purpose": "Theme-aware, responsive popover component."
},
{
"name": "Tooltip",
"category": "overlays",
"purpose": "Theme-aware, responsive tooltip component."
},
{
"name": "Table",
"category": "tables",
"purpose": "Theme-aware, responsive table component."
},
{
"name": "AdvancedRangeSlider",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced range slider component."
},
{
"name": "AdvancedDatePicker",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced date picker component."
},
{
"name": "Chart",
"category": "integrations",
"purpose": "Theme-aware, responsive chart component."
},
{
"name": "Clipboard",
"category": "integrations",
"purpose": "Theme-aware, responsive clipboard component."
},
{
"name": "Confetti",
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component."
"name": "CustomScrollbar",
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component."
},
{
"name": "DataMap",
@@ -410,35 +173,201 @@
"category": "integrations",
"purpose": "Theme-aware, responsive data table component."
},
{
"name": "DatePicker",
"category": "base",
"purpose": "Theme-aware, responsive date picker component."
},
{
"name": "DeviceFrame",
"category": "base",
"purpose": "Theme-aware, responsive device frame component."
},
{
"name": "Divider",
"category": "layout",
"purpose": "Theme-aware, responsive divider component."
},
{
"name": "DragAndDrop",
"category": "integrations",
"purpose": "Theme-aware, responsive drag and drop component."
},
{
"name": "Drawer",
"category": "overlays",
"purpose": "Theme-aware, responsive drawer component."
},
{
"name": "Dropdown",
"category": "overlays",
"purpose": "Theme-aware, responsive dropdown component."
},
{
"name": "FeatureCard",
"category": "blocks",
"purpose": "Reusable linked feature or service card with icon, badge, description, and action."
},
{
"name": "FeatureGrid",
"category": "layout",
"purpose": "Responsive equal-height grid for feature and service cards."
},
{
"name": "FeatureIconCard",
"category": "blocks",
"purpose": "Feature card with a prominent themed icon treatment."
},
{
"name": "FileInput",
"category": "forms",
"purpose": "Theme-aware, responsive file input component."
},
{
"name": "FileUpload",
"category": "integrations",
"purpose": "Theme-aware, responsive file upload component."
},
{
"name": "FileUploadProgress",
"category": "base",
"purpose": "Theme-aware, responsive file upload progress component."
},
{
"name": "Footer",
"category": "navigation",
"purpose": "Responsive application footer with structured links and pre/post content slots."
},
{
"name": "Grid",
"category": "layout",
"purpose": "Theme-aware, responsive grid component."
},
{
"name": "Hero",
"category": "blocks",
"purpose": "Reusable public-page hero with actions, trust items, badges, and visual slots."
},
{
"name": "HeroActions",
"category": "base",
"purpose": "Responsive action group for hero and call-to-action sections."
},
{
"name": "Image",
"category": "layout",
"purpose": "Theme-aware, responsive image component."
},
{
"name": "Input",
"category": "forms",
"purpose": "Theme-aware, responsive input component."
},
{
"name": "InputGroup",
"category": "forms",
"purpose": "Theme-aware, responsive input group component."
},
{
"name": "InputNumber",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive input number component."
},
{
"name": "Kbd",
"category": "layout",
"purpose": "Theme-aware, responsive kbd component."
},
{
"name": "LayoutSplitter",
"category": "layout",
"purpose": "Theme-aware, responsive layout splitter component."
},
{
"name": "LegendIndicator",
"category": "base",
"purpose": "Theme-aware, responsive legend indicator component."
},
{
"name": "Link",
"category": "layout",
"purpose": "Theme-aware, responsive link component."
},
{
"name": "List",
"category": "base",
"purpose": "Theme-aware, responsive list component."
},
{
"name": "ListGroup",
"category": "base",
"purpose": "Theme-aware, responsive list group component."
},
{
"name": "Map",
"category": "integrations",
"purpose": "Theme-aware, responsive map component."
},
{
"name": "ToastNotifications",
"category": "integrations",
"purpose": "Theme-aware, responsive toast notifications component."
"name": "MarketingSectionHeader",
"category": "blocks",
"purpose": "Marketing section heading with an optional linked action."
},
{
"name": "WysiwygEditor",
"category": "integrations",
"purpose": "Theme-aware, responsive wysiwyg editor component."
"name": "Marquee",
"category": "base",
"purpose": "Theme-aware, responsive marquee component."
},
{
"name": "PreferenceSwitcher",
"name": "MegaMenu",
"category": "navigation",
"purpose": "Accessible theme, accent color, and language preference controls."
"purpose": "Theme-aware, responsive mega menu component."
},
{
"name": "MetricCard",
"category": "blocks",
"purpose": "Single statistic or operational metric with icon, trend, and optional action."
},
{
"name": "MetricGrid",
"category": "layout",
"purpose": "Responsive collection of metric cards."
},
{
"name": "Modal",
"category": "overlays",
"purpose": "Theme-aware, responsive modal component."
},
{
"name": "Nav",
"category": "navigation",
"purpose": "Theme-aware, responsive nav component."
},
{
"name": "Navbar",
"category": "navigation",
"purpose": "Theme-aware, responsive navbar component."
},
{
"name": "PageHeader",
"category": "blocks",
"purpose": "Public and application page header with breadcrumbs, metadata, and actions."
},
{
"name": "Pagination",
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component."
},
{
"name": "PinInput",
"category": "advanced-forms",
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support.",
"events": ["input", "change", "complete", "paste", "clear", "error"]
},
{
"name": "Popover",
"category": "overlays",
"purpose": "Theme-aware, responsive popover component."
},
{
"name": "PortalDashboard",
@@ -446,14 +375,175 @@
"purpose": "Responsive portal dashboard with metrics, actions, tasks, and updates."
},
{
"name": "AuthSplitLayout",
"category": "layout",
"purpose": "Responsive 50/50 authentication layout with content and form regions."
"name": "PreferenceSwitcher",
"category": "navigation",
"purpose": "Accessible theme, accent color, and language preference controls."
},
{
"name": "AuthForm",
"name": "Progress",
"category": "base",
"purpose": "Theme-aware, responsive progress component."
},
{
"name": "PublicPageShell",
"category": "layout",
"purpose": "Consistent structural wrapper for public-facing pages."
},
{
"name": "Radio",
"category": "forms",
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA."
"purpose": "Theme-aware, responsive radio component."
},
{
"name": "RangeSlider",
"category": "forms",
"purpose": "Theme-aware, responsive range slider component."
},
{
"name": "Rating",
"category": "base",
"purpose": "Theme-aware, responsive rating component."
},
{
"name": "Scrollspy",
"category": "navigation",
"purpose": "Theme-aware, responsive scrollspy component."
},
{
"name": "SearchBox",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive search box component."
},
{
"name": "Section",
"category": "layout",
"purpose": "Theme-aware page section with consistent spacing, width, background, and border controls."
},
{
"name": "SectionHeader",
"category": "layout",
"purpose": "Reusable accessible section heading with eyebrow, title, description, icon, and actions."
},
{
"name": "Select",
"category": "forms",
"purpose": "Theme-aware, responsive select component."
},
{
"name": "Sidebar",
"category": "navigation",
"purpose": "Theme-aware, responsive sidebar component."
},
{
"name": "Skeleton",
"category": "base",
"purpose": "Theme-aware, responsive skeleton component."
},
{
"name": "Spinner",
"category": "base",
"purpose": "Theme-aware, responsive spinner component."
},
{
"name": "SplitHero",
"category": "blocks",
"purpose": "Two-column hero with configurable content and visual placement."
},
{
"name": "StatsBar",
"category": "blocks",
"purpose": "Compact responsive statistics strip."
},
{
"name": "Stepper",
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component."
},
{
"name": "StrongPassword",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive strong password component."
},
{
"name": "StyledIcon",
"category": "base",
"purpose": "Theme-aware, responsive styled icon component."
},
{
"name": "Switch",
"category": "forms",
"purpose": "Theme-aware, responsive switch component."
},
{
"name": "Table",
"category": "tables",
"purpose": "Theme-aware, responsive table component."
},
{
"name": "Tabs",
"category": "navigation",
"purpose": "Theme-aware, responsive tabs component."
},
{
"name": "Textarea",
"category": "forms",
"purpose": "Theme-aware, responsive textarea component."
},
{
"name": "TextLink",
"category": "layout",
"purpose": "Styled inline action link with icons, arrows, external state, and disabled handling."
},
{
"name": "Timeline",
"category": "base",
"purpose": "Theme-aware, responsive timeline component."
},
{
"name": "TimePicker",
"category": "forms",
"purpose": "Theme-aware, responsive time picker component."
},
{
"name": "Toast",
"category": "base",
"purpose": "Theme-aware, responsive toast component."
},
{
"name": "ToastNotifications",
"category": "integrations",
"purpose": "Theme-aware, responsive toast notifications component."
},
{
"name": "ToggleCount",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive toggle count component."
},
{
"name": "TogglePassword",
"category": "advanced-forms",
"purpose": "Accessible password field with optional show and hide controls.",
"events": ["input", "change", "toggle"]
},
{
"name": "Tooltip",
"category": "overlays",
"purpose": "Theme-aware, responsive tooltip component."
},
{
"name": "TreeView",
"category": "base",
"purpose": "Theme-aware, responsive tree view component."
},
{
"name": "Typography",
"category": "layout",
"purpose": "Theme-aware, responsive typography component."
},
{
"name": "WysiwygEditor",
"category": "integrations",
"purpose": "Theme-aware, responsive wysiwyg editor component."
}
]
}
File diff suppressed because it is too large Load Diff
+131
View File
@@ -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>
}
}
+69
View File
@@ -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;
}
}
}
+150
View File
@@ -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>
}
}
+23 -1
View File
@@ -7,7 +7,29 @@ component Columns {
maxWidth = "xl"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--columns wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
data-ui-component="Columns"
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 items-start {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:md:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:gap-3='gap === "sm"'
class:gap-6='gap === "md"'
class:gap-10='gap === "lg"'
class:gap-14='gap === "xl"'
>
<slot></slot>
</div>
}
}
+113
View File
@@ -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>
}
}
+55
View File
@@ -0,0 +1,55 @@
component FeatureGrid {
props {
color = "primary"
size = "default"
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>
}
}
+119 -76
View File
@@ -12,90 +12,133 @@ component Footer {
class = ""
}
functions {
function groupedItems() {
const groups = []
let group = null
items.forEach((item) => {
if (item.type === "header") {
group = {
heading: item,
links: []
}
groups.push(group)
return
}
if (!group) {
group = {
heading: null,
links: []
}
groups.push(group)
}
group.links.push(item)
})
return groups
}
function selectItem(item) {
$emit(item.type === "action" ? "action" : "select", {
item: item,
value: item.value || ""
})
}
}
view {
<footer class="wire-footer wire-footer--width-{maxWidth} wire-next--color-{color} wire-next--size-{size} wire-footer--columns-{columns} {class}">
<div class="wire-footer__grid">
<div class="wire-footer__pre">
<slot name="pre-footer" />
</div>
<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>
<nav class="wire-footer__links" aria-label="{label}">
{#each groupedItems() as group}
<section class="wire-footer__column">
{#if group.heading}
<strong class="wire-footer__heading">{group.heading.label}</strong>
<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}
<div class="wire-footer__column-links">
{#each group.links as item}
<a class="wire-footer__link wire-footer__link--{item.type || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectItem(item)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
{#if item.external}<span aria-hidden="true">↗</span>{/if}
</a>
{/each}
</div>
</section>
{/each}
{/each}
</div>
</nav>
<div class="wire-footer__post">
<slot name="post-footer" />
<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>
{#if copyright}
<div class="wire-footer__bottom">
<div class="wire-footer__copyright-left">
<slot name="copyright-left" />
</div>
<div class="wire-footer__copyright">{copyright}</div>
<div class="wire-footer__copyright-right">
<slot name="copyright-right" />
</div>
</div>
{:else}
<div class="wire-footer__bottom wire-footer__bottom--slots">
<div class="wire-footer__copyright-left">
<slot name="copyright-left" />
</div>
<div class="wire-footer__copyright-right">
<slot name="copyright-right" />
</div>
</div>
{/if}
<slot name="post-footer"></slot>
</footer>
}
}
+149
View File
@@ -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>
}
}
+52
View File
@@ -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>
}
}
+31 -5
View File
@@ -1,7 +1,5 @@
component Image {
props {
@event load = function
@event error = function
size = "default"
color = "primary"
src = ""
@@ -12,10 +10,38 @@ component Image {
rounded = false
class = ""
}
view {
<figure class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--image {rounded ? 'wire-next--rounded' : ''} {class}">
<img src="{src}" alt="{alt}" width="{width}" height="{height}" loading="{loading}" />
<slot />
<figure
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='rounded'
>
{#if src}
<img
src='{src}'
alt='{alt}'
width='{width}'
height='{height}'
loading='{loading}'
decoding="async"
class="block h-auto w-full object-cover transition duration-300"
class:aspect-square='size === "square"'
class:aspect-video='size === "video"'
class:aspect-[4/3]='size === "landscape"'
class:aspect-[3/4]='size === "portrait"'
/>
{:else}
<div
class="flex min-h-48 w-full items-center justify-center text-[var(--wire-color-text-muted)]"
role="img"
aria-label='{alt || "Image placeholder"}'
>
<span class="icon-[lucide--image] size-8" aria-hidden="true"></span>
</div>
{/if}
<slot></slot>
</figure>
}
}
+33 -4
View File
@@ -1,8 +1,5 @@
component Link {
props {
@event click = function
@event focus = function
@event blur = function
size = "default"
color = "primary"
label = "Link"
@@ -12,7 +9,39 @@ component Link {
external = false
class = ""
}
view {
<a {...attrs} href="{href}" target="{target}" rel="{rel}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--link {class}"><slot>{label}</slot>{#if external}<span aria-hidden="true">↗</span>{/if}</a>
<a
data-ui-component="Link"
href='{href}'
target='{target}'
rel='{external ? (rel || "noopener noreferrer") : rel}'
class='inline-flex min-w-0 items-center gap-1.5 rounded-md font-semibold underline-offset-4 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-background)] {class}'
class:text-xs='size === "xs"'
class:text-sm='size === "sm" || size === "default"'
class:text-base='size === "md"'
class:text-lg='size === "lg"'
class:text-[var(--wire-color-primary)]='color === "primary"'
class:hover:text-[var(--wire-color-primary-hover)]='color === "primary"'
class:text-[var(--wire-color-secondary)]='color === "secondary"'
class:hover:text-[var(--wire-color-secondary-hover)]='color === "secondary"'
class:text-[var(--wire-color-success)]='color === "success"'
class:text-[var(--wire-color-warning-text)]='color === "warning"'
class:text-[var(--wire-color-danger)]='color === "danger"'
class:text-[var(--wire-color-info)]='color === "info"'
class:text-[var(--wire-color-text)]='color === "neutral"'
class:hover:underline='external === false'
>
<span class="truncate">{label}</span>
{#if external}
<span
class="icon-[lucide--external-link] size-3.5 shrink-0"
aria-hidden="true"
></span>
{/if}
<slot></slot>
</a>
}
}
+93 -7
View File
@@ -1,7 +1,5 @@
component List {
props {
@event select = function
@event click = function
size = "default"
color = "primary"
title = "List"
@@ -10,12 +8,100 @@ component List {
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--list wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="List"
aria-label='{title}'
class='w-full {class}'
>
{#if title || description}
<header class="mb-4">
{#if title}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
{/if}
{#if description}
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
</header>
{/if}
<ul
class="overflow-hidden"
class:divide-y='variant === "default" || variant === "divided"'
class:divide-[var(--wire-color-border)]='variant === "default" || variant === "divided"'
class:rounded-2xl='variant === "card"'
class:border='variant === "card"'
class:border-[var(--wire-color-border)]='variant === "card"'
class:bg-[var(--wire-color-surface-raised)]='variant === "card"'
class:space-y-3='variant === "separated"'
>
{#each items as item, index}
<li
class="group min-w-0"
class:rounded-xl='variant === "separated"'
class:border='variant === "separated"'
class:border-[var(--wire-color-border)]='variant === "separated"'
class:bg-[var(--wire-color-surface-raised)]='variant === "separated"'
>
{#if item.href}
<a
href='{item.href}'
class="flex min-w-0 items-start gap-3 px-4 py-3 outline-none transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
class:px-3='size === "sm"'
class:py-2.5='size === "sm"'
class:px-5='size === "lg"'
class:py-4='size === "lg"'
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
>
{#if item.icon}
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
{#if item.imageSrc}
<img src='{item.imageSrc}' alt='{item.imageAlt || ""}' class="size-12 shrink-0 rounded-xl object-cover" loading="lazy" />
{/if}
<div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-3">
<p class="truncate font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
{#if item.meta}
<span class="shrink-0 text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
{/if}
</div>
{#if item.description}
<p class="mt-1 line-clamp-2 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
{#if item.badge}
<span class="mt-2 inline-flex rounded-full bg-[var(--wire-color-primary-soft)] px-2.5 py-1 text-xs font-semibold text-[var(--wire-color-primary)]">{item.badge}</span>
{/if}
</div>
<span class="icon-[lucide--chevron-right] mt-1 size-4 shrink-0 text-[var(--wire-color-text-muted)] transition group-hover:translate-x-0.5 group-hover:text-[var(--wire-color-primary)]" aria-hidden="true"></span>
</a>
{:else}
<div class="flex min-w-0 items-start gap-3 px-4 py-3">
{#if item.icon}
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
<div class="min-w-0 flex-1">
<p class="font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
{#if item.description}
<p class="mt-1 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
</div>
</div>
{/if}
</li>
{:empty}
<li class="rounded-xl border border-dashed border-[var(--wire-color-border)] p-6 text-center text-sm text-[var(--wire-color-text-muted)]">
No items available.
</li>
{/each}
</ul>
<slot></slot>
</section>
}
}
+71 -9
View File
@@ -1,9 +1,5 @@
component Map {
props {
@event ready = function
@event move = function
@event zoom = function
@event markerClick = function
size = "default"
color = "primary"
title = "Map"
@@ -12,12 +8,78 @@ component Map {
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--map wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="Map"
aria-label='{title}'
class='overflow-hidden rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] shadow-sm {class}'
>
{#if title || description}
<header class="flex items-start justify-between gap-4 border-b border-[var(--wire-color-border)] p-5">
<div>
{#if title}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
{/if}
{#if description}
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
</div>
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
<span class="icon-[lucide--map] size-5" aria-hidden="true"></span>
</span>
</header>
{/if}
<div
class="relative isolate overflow-hidden bg-[var(--wire-color-surface-soft)]"
class:h-64='size === "sm"'
class:h-80='size === "default" || size === "md"'
class:h-[28rem]='size === "lg"'
>
<div
class="pointer-events-none absolute inset-0 opacity-50"
style="background-image: linear-gradient(var(--wire-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--wire-color-border) 1px, transparent 1px); background-size: 32px 32px;"
aria-hidden="true"
></div>
<div class="absolute inset-0 flex items-center justify-center p-6">
<slot></slot>
</div>
{#if items.length > 0}
{#each items as item, index}
<button
type="button"
aria-label='{item.label || item.title || "Map marker"}'
class="absolute inline-flex size-10 items-center justify-center rounded-full border-4 border-[var(--wire-color-surface-raised)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-lg transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
@click='event.currentTarget.dispatchEvent(new CustomEvent("markerClick", { bubbles: true, detail: item })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: item }))'
>
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
</button>
{/each}
{/if}
<div class="absolute bottom-4 right-4 flex flex-col gap-2">
<button
type="button"
aria-label="Zoom in"
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "in" } }))'
>
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
</button>
<button
type="button"
aria-label="Zoom out"
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "out" } }))'
>
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
</button>
</div>
</div>
</section>
}
}
@@ -0,0 +1,59 @@
component MarketingSectionHeader {
props {
id = ""
eyebrow = ""
title = ""
description = ""
align = "split"
size = "default"
color = "primary"
actionLabel = ""
actionHref = ""
actionIcon = ""
actionExternal = false
class = ""
}
view {
<div
{...attrs}
data-ui-component="MarketingSectionHeader"
data-size='{size}'
data-color='{color}'
class='{class}'
>
<SectionHeader
id='{id}'
eyebrow='{eyebrow}'
title='{title}'
description='{description}'
align='{align}'
size='{size}'
color='{color}'
headingLevel="2"
maxWidth="3xl"
>
<div data-slot="icon">
<slot name="icon"></slot>
</div>
<div data-slot="actions">
{#if actionLabel}
<TextLink
label='{actionLabel}'
href='{actionHref}'
external='{actionExternal}'
icon='{actionIcon}'
iconPosition="start"
showArrow="true"
color='{color}'
/>
{/if}
<slot name="actions"></slot>
</div>
<slot></slot>
</SectionHeader>
</div>
}
}
+97 -7
View File
@@ -1,7 +1,5 @@
component Marquee {
props {
@event pause = function
@event resume = function
size = "default"
color = "primary"
title = "Marquee"
@@ -10,12 +8,104 @@ component Marquee {
variant = "default"
class = ""
}
state paused = false
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--marquee wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="Marquee"
aria-label='{title}'
class='overflow-hidden border-y border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] {class}'
>
<div class="flex items-stretch">
{#if title}
<div class="relative z-10 flex shrink-0 items-center gap-2 border-r border-[var(--wire-color-border)] bg-[var(--wire-color-primary)] px-4 py-3 text-[var(--wire-color-on-primary)] sm:px-5">
<span class="icon-[lucide--megaphone] size-4" aria-hidden="true"></span>
<span class="text-sm font-bold">{title}</span>
</div>
{/if}
<div
class="group relative min-w-0 flex-1 overflow-hidden"
@mouseenter='paused = true; event.currentTarget.dispatchEvent(new CustomEvent("pause", { bubbles: true }))'
@mouseleave='paused = false; event.currentTarget.dispatchEvent(new CustomEvent("resume", { bubbles: true }))'
@focusin='paused = true'
@focusout='paused = false'
>
<div
class="wire-marquee-track flex w-max min-w-full items-center"
class:wire-marquee-paused='paused'
>
{#each items as item}
<a
href='{item.href || "#"}'
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)] transition hover:bg-[var(--wire-color-primary-soft)] hover:text-[var(--wire-color-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
>
<span
class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]"
class:bg-[var(--wire-color-danger)]='item.color === "danger"'
class:bg-[var(--wire-color-warning)]='item.color === "warning"'
class:bg-[var(--wire-color-success)]='item.color === "success"'
aria-hidden="true"
></span>
<span>{item.label || item.title}</span>
{#if item.meta}
<span class="text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
{/if}
</a>
{:empty}
<p class="px-5 py-3 text-sm text-[var(--wire-color-text-muted)]">{description || "No announcements available."}</p>
{/each}
{#if items.length > 0}
{#each items as item}
<a
href='{item.href || "#"}'
aria-hidden="true"
tabindex="-1"
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)]"
>
<span class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]" aria-hidden="true"></span>
<span>{item.label || item.title}</span>
</a>
{/each}
{/if}
</div>
</div>
<button
type="button"
aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
class="flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] px-4 text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
@click='paused = !paused; event.currentTarget.dispatchEvent(new CustomEvent(paused ? "pause" : "resume", { bubbles: true }))'
>
<span class="icon-[lucide--pause] size-4" data-show='!paused' aria-hidden="true"></span>
<span class="icon-[lucide--play] size-4" data-show='paused' aria-hidden="true"></span>
</button>
</div>
<slot></slot>
</section>
}
style {
.wire-marquee-track {
animation: wire-marquee 32s linear infinite;
}
.wire-marquee-paused {
animation-play-state: paused;
}
@keyframes wire-marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.wire-marquee-track {
animation: none;
}
}
}
}
+107
View File
@@ -0,0 +1,107 @@
component MetricCard {
props {
label = "Metric"
value = "0"
description = ""
icon = ""
prefix = ""
suffix = ""
trend = ""
trendLabel = ""
trendDirection = "neutral"
href = ""
actionLabel = ""
size = "default"
color = "primary"
variant = "default"
align = "left"
class = ""
}
view {
<article
data-ui-component="MetricCard"
class='group relative flex h-full flex-col rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] p-5 shadow-sm transition {class}'
class:p-4='size === "sm"'
class:p-6='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 === "minimal"'
class:border-transparent='variant === "minimal"'
class:shadow-none='variant === "minimal"'
class:text-center='align === "center"'
class:items-center='align === "center"'
class:text-right='align === "right"'
class:items-end='align === "right"'
class:hover:border-[var(--wire-color-primary)]='href !== ""'
class:hover:shadow-lg='href !== ""'
>
<div class="flex w-full items-start justify-between gap-4">
<div>
<p class="text-sm font-semibold text-[var(--wire-color-text-muted)]">{label}</p>
<p class="mt-2 text-3xl font-bold tracking-tight text-[var(--wire-color-text)]" class:text-4xl='size === "lg"'>
<span>{prefix}</span><span>{value}</span><span>{suffix}</span>
</p>
</div>
{#if icon}
<span
class="flex size-11 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"'
>
<span class='{icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
</div>
{#if description}
<p class="mt-3 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
{#if trend || trendLabel}
<div class="mt-4 inline-flex items-center gap-2 text-sm font-semibold">
<span
class="inline-flex items-center gap-1 rounded-full px-2.5 py-1"
class:bg-[var(--wire-color-success-soft)]='trendDirection === "up" || trendDirection === "positive"'
class:text-[var(--wire-color-success)]='trendDirection === "up" || trendDirection === "positive"'
class:bg-[var(--wire-color-danger-soft)]='trendDirection === "down" || trendDirection === "negative"'
class:text-[var(--wire-color-danger)]='trendDirection === "down" || trendDirection === "negative"'
class:bg-[var(--wire-color-surface-soft)]='trendDirection === "neutral"'
class:text-[var(--wire-color-text-muted)]='trendDirection === "neutral"'
>
{#if trendDirection === "up" || trendDirection === "positive"}
<span class="icon-[lucide--trending-up] size-4" aria-hidden="true"></span>
{:else if trendDirection === "down" || trendDirection === "negative"}
<span class="icon-[lucide--trending-down] size-4" aria-hidden="true"></span>
{:else}
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
{/if}
<span>{trend}</span>
</span>
{#if trendLabel}
<span class="text-[var(--wire-color-text-muted)]">{trendLabel}</span>
{/if}
</div>
{/if}
{#if href}
<TextLink
label='{actionLabel || "View details"}'
href='{href}'
color='{color}'
size="sm"
class="mt-5"
/>
{/if}
</article>
}
}
+60
View File
@@ -0,0 +1,60 @@
component MetricGrid {
props {
items = []
columns = 4
tabletColumns = 2
mobileColumns = 1
gap = "md"
equalHeight = true
dividers = false
size = "default"
color = "primary"
variant = "default"
class = ""
}
view {
<div
data-ui-component="MetricGrid"
class='grid w-full grid-cols-1 {class}'
class:grid-cols-2='mobileColumns === 2'
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:divide-y='dividers'
class:sm:divide-y-0='dividers'
class:sm:divide-x='dividers'
class:divide-[var(--wire-color-border)]='dividers'
>
{#each items as item}
<MetricCard
label='{item.label || item.title}'
value='{item.value}'
description='{item.description || ""}'
icon='{item.icon || ""}'
prefix='{item.prefix || ""}'
suffix='{item.suffix || ""}'
trend='{item.trend || ""}'
trendLabel='{item.trendLabel || ""}'
trendDirection='{item.trendDirection || "neutral"}'
href='{item.href || ""}'
actionLabel='{item.actionLabel || ""}'
size='{item.size || size}'
color='{item.color || color}'
variant='{dividers ? "minimal" : (item.variant || variant)}'
align='{item.align || "left"}'
class="h-full"
/>
{:empty}
<slot></slot>
{/each}
</div>
}
}
+105
View File
@@ -0,0 +1,105 @@
component PageHeader {
props {
eyebrow = ""
title = ""
description = ""
icon = ""
align = "left"
size = "default"
compact = false
showBreadcrumbs = false
breadcrumbs = []
primaryLabel = ""
primaryHref = ""
primaryIcon = ""
secondaryLabel = ""
secondaryHref = ""
secondaryIcon = ""
color = "primary"
variant = "default"
class = ""
}
view {
<div
{...attrs}
data-ui-component="PageHeader"
data-size='{size}'
data-color='{color}'
class='{class}'
>
<Section
spacing='{compact || size === "compact" ? "md" : "lg"}'
variant='{variant}'
color='{color}'
borderBottom="true"
>
{#if showBreadcrumbs && breadcrumbs.length > 0}
<Breadcrumb
label="Breadcrumb"
items='{breadcrumbs}'
active='{breadcrumbs[breadcrumbs.length - 1]?.value || breadcrumbs[breadcrumbs.length - 1]?.label || ""}'
color='{color}'
class="mb-6"
/>
{/if}
<div
class="flex flex-col gap-6"
class:items-center='align === "center"'
class:text-center='align === "center"'
class:lg:flex-row='align !== "center"'
class:lg:items-end='align !== "center"'
class:lg:justify-between='align !== "center"'
>
<div class="max-w-4xl">
{#if icon}
<span class="mb-5 inline-flex size-12 items-center justify-center rounded-2xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
<span class='{icon + " size-6"}' aria-hidden="true"></span>
</span>
{/if}
<SectionHeader
eyebrow='{eyebrow}'
title='{title}'
description='{description}'
align='{align === "center" ? "center" : "left"}'
size='{compact || size === "compact" ? "sm" : "lg"}'
color='{color}'
headingLevel="1"
maxWidth="4xl"
>
<slot name="meta"></slot>
</SectionHeader>
</div>
<div class="flex flex-wrap items-center gap-3" class:justify-center='align === "center"'>
{#if secondaryLabel}
<Button
label='{secondaryLabel}'
href='{secondaryHref}'
icon='{secondaryIcon}'
variant="outline"
color='{color}'
/>
{/if}
{#if primaryLabel}
<Button
label='{primaryLabel}'
href='{primaryHref}'
icon='{primaryIcon}'
variant="default"
color='{color}'
/>
{/if}
<slot name="actions"></slot>
</div>
</div>
<slot></slot>
</Section>
</div>
}
}
@@ -0,0 +1,53 @@
component PublicPageShell {
props {
maxWidth = "full"
fullWidth = true
headerOffset = "none"
background = "default"
overflow = "clip"
minHeight = "screen"
size = "default"
color = "primary"
variant = "default"
class = ""
}
view {
<div
data-ui-component="PublicPageShell"
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='relative isolate w-full text-[var(--wire-color-text)] {class}'
class:min-h-screen='minHeight === "screen"'
class:min-h-dvh='minHeight === "dvh"'
class:overflow-x-clip='overflow === "clip"'
class:overflow-x-hidden='overflow === "hidden"'
class:bg-[var(--wire-color-background)]='background === "default"'
class:bg-[var(--wire-color-surface-soft)]='background === "soft"'
class:bg-[var(--wire-color-surface-raised)]='background === "raised"'
class:pt-16='headerOffset === "sm"'
class:pt-20='headerOffset === "md"'
class:pt-24='headerOffset === "lg"'
>
<slot name="before"></slot>
{#if fullWidth}
<main class="w-full">
<slot></slot>
</main>
{:else}
<main
class="mx-auto w-full px-4 sm:px-6 lg:px-8"
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
>
<slot></slot>
</main>
{/if}
<slot name="after"></slot>
</div>
}
}
+61 -6
View File
@@ -1,10 +1,5 @@
component SearchBox {
props {
@event input = function
@event change = function
@event search = function
@event submit = function
@event clear = function
size = "default"
color = "primary"
label = "Search Box"
@@ -19,7 +14,67 @@ component SearchBox {
required = false
class = ""
}
state query = value
view {
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--search-box {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
<form
data-ui-component="SearchBox"
role="search"
class='w-full {class}'
@submit='event.preventDefault(); event.currentTarget.dispatchEvent(new CustomEvent("search", { bubbles: true, detail: { value: query, name: name } }))'
>
<label
class="mb-2 block text-sm font-semibold text-[var(--wire-color-text)]"
class:sr-only='label === ""'
>
{label}
</label>
<div class="relative flex items-center">
<span
class="icon-[lucide--search] pointer-events-none absolute left-4 size-5 text-[var(--wire-color-input-placeholder)]"
aria-hidden="true"
></span>
<input
name='{name}'
type='{type}'
value='{query}'
placeholder='{placeholder}'
min='{min}'
max='{max}'
step='{step}'
disabled='{disabled}'
required='{required}'
autocomplete="off"
class="w-full rounded-xl border border-[var(--wire-color-input-border)] bg-[var(--wire-color-input-background)] pl-12 pr-24 text-[var(--wire-color-input-text)] outline-none transition placeholder:text-[var(--wire-color-input-placeholder)] hover:border-[var(--wire-color-input-border-hover)] focus:border-[var(--wire-color-input-border-focus)] focus:ring-2 focus:ring-[var(--wire-color-focus)] disabled:cursor-not-allowed disabled:opacity-60"
class:h-10='size === "sm"'
class:h-12='size === "default" || size === "md"'
class:h-14='size === "lg"'
@input='query = event.target.value'
@change='query = event.target.value'
/>
<button
type="button"
aria-label="Clear search"
data-show='query.length > 0 && !disabled'
class="absolute right-12 inline-flex size-8 items-center justify-center rounded-lg text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); event.currentTarget.dispatchEvent(new CustomEvent("clear", { bubbles: true, detail: { value: "", name: name } }))'
>
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
</button>
<button
type="submit"
aria-label='{label || "Search"}'
disabled='{disabled}'
class="absolute right-2 inline-flex size-9 items-center justify-center rounded-lg bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] transition hover:bg-[var(--wire-color-primary-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] disabled:cursor-not-allowed disabled:opacity-60"
>
<span class="icon-[lucide--arrow-right] size-4" aria-hidden="true"></span>
</button>
</div>
</form>
}
}
+60
View File
@@ -0,0 +1,60 @@
component Section {
props {
id = ""
size = "default"
color = "primary"
variant = "default"
spacing = "lg"
maxWidth = "xl"
fullWidth = false
borderTop = false
borderBottom = false
class = ""
}
view {
<section
data-ui-component="Section"
id='{id}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='relative isolate w-full {class}'
class:py-8='spacing === "sm"'
class:py-12='spacing === "md"'
class:py-16='spacing === "lg"'
class:sm:py-20='spacing === "lg"'
class:py-20='spacing === "xl"'
class:sm:py-24='spacing === "xl"'
class:bg-[var(--wire-color-background)]='variant === "default"'
class:bg-[var(--wire-color-surface-soft)]='variant === "soft"'
class:bg-[var(--wire-color-surface-raised)]='variant === "raised"'
class:bg-[var(--wire-color-primary-soft)]='variant === "tinted" && color === "primary"'
class:bg-[var(--wire-color-success-soft)]='variant === "tinted" && color === "success"'
class:bg-[var(--wire-color-warning-soft)]='variant === "tinted" && color === "warning"'
class:bg-[var(--wire-color-danger-soft)]='variant === "tinted" && color === "danger"'
class:bg-[var(--wire-color-info-soft)]='variant === "tinted" && color === "info"'
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:border-t='borderTop'
class:border-b='borderBottom'
class:border-[var(--wire-color-border)]='borderTop || borderBottom'
>
{#if fullWidth}
<slot></slot>
{:else}
<Container
columns="1"
gap="md"
maxWidth='{maxWidth}'
size='{size}'
color='{color}'
>
<slot></slot>
</Container>
{/if}
</section>
}
}
+113
View File
@@ -0,0 +1,113 @@
component SectionHeader {
props {
id = ""
eyebrow = ""
title = ""
description = ""
align = "left"
size = "default"
color = "primary"
headingLevel = 2
maxWidth = "3xl"
class = ""
}
view {
<header
data-ui-component="SectionHeader"
class='flex min-w-0 flex-col gap-5 {class}'
class:items-center='align === "center"'
class:text-center='align === "center"'
class:items-end='align === "right"'
class:text-right='align === "right"'
class:lg:flex-row='align === "split"'
class:lg:items-end='align === "split"'
class:lg:justify-between='align === "split"'
>
<div
class="min-w-0"
class:max-w-xl='maxWidth === "xl"'
class:max-w-2xl='maxWidth === "2xl"'
class:max-w-3xl='maxWidth === "3xl"'
class:max-w-4xl='maxWidth === "4xl"'
class:mx-auto='align === "center"'
>
<div class="mb-3 flex items-center gap-3" class:justify-center='align === "center"' class:justify-end='align === "right"'>
<slot name="icon"></slot>
{#if eyebrow}
<p
class="text-xs font-bold uppercase tracking-[0.18em]"
class:text-sm='size === "lg"'
class:text-[var(--wire-color-primary)]='color === "primary"'
class:text-[var(--wire-color-secondary)]='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"'
>
{eyebrow}
</p>
{/if}
</div>
{#if headingLevel === 1}
<h1
id='{id}'
class="font-bold leading-tight tracking-tight text-[var(--wire-color-text)]"
class:text-3xl='size === "sm"'
class:text-4xl='size === "default" || size === "md"'
class:sm:text-5xl='size === "default" || size === "md"'
class:text-5xl='size === "lg"'
class:sm:text-6xl='size === "lg"'
>
{title}
</h1>
{:else if headingLevel === 3}
<h3
id='{id}'
class="font-bold leading-tight tracking-tight text-[var(--wire-color-text)]"
class:text-xl='size === "sm"'
class:text-2xl='size === "default" || size === "md"'
class:text-3xl='size === "lg"'
>
{title}
</h3>
{:else}
<h2
id='{id}'
class="font-bold leading-tight tracking-tight text-[var(--wire-color-text)]"
class:text-2xl='size === "sm"'
class:text-3xl='size === "default" || size === "md"'
class:sm:text-4xl='size === "default" || size === "md"'
class:text-4xl='size === "lg"'
class:sm:text-5xl='size === "lg"'
>
{title}
</h2>
{/if}
{#if description}
<p
class="mt-4 leading-7 text-[var(--wire-color-text-muted)]"
class:text-sm='size === "sm"'
class:text-base='size === "default" || size === "md"'
class:text-lg='size === "lg"'
>
{description}
</p>
{/if}
<slot></slot>
</div>
<div
class="flex shrink-0 flex-wrap items-center gap-3"
class:justify-center='align === "center"'
class:justify-end='align === "right" || align === "split"'
>
<slot name="actions"></slot>
</div>
</header>
}
}
+98
View File
@@ -0,0 +1,98 @@
component SplitHero {
props {
eyebrow = ""
title = "A better digital experience"
highlight = ""
description = ""
primaryLabel = ""
primaryHref = ""
primaryIcon = ""
secondaryLabel = ""
secondaryHref = ""
secondaryIcon = ""
visualPosition = "right"
reverse = false
ratio = "balanced"
size = "default"
color = "primary"
variant = "default"
trustItems = []
class = ""
}
view {
<Section
spacing='{size === "compact" ? "md" : "xl"}'
variant='{variant === "soft" ? "soft" : "default"}'
color='{color}'
class='overflow-hidden {class}'
>
<div
data-ui-component="SplitHero"
class="grid items-center gap-10 lg:grid-cols-2 lg:gap-16"
class:lg:grid-cols-[1.15fr_0.85fr]='ratio === "content"'
class:lg:grid-cols-[0.85fr_1.15fr]='ratio === "visual"'
>
<div
class="max-w-3xl"
class:lg:order-2='reverse || visualPosition === "left"'
>
{#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="icon-[lucide--sparkles] size-4" aria-hidden="true"></span>
<span>{eyebrow}</span>
</div>
{/if}
<h1 class="text-4xl font-bold leading-[1.08] tracking-[-0.035em] text-[var(--wire-color-text)] sm:text-5xl lg:text-6xl">
<span>{title}</span>
{#if highlight}
<span class="block text-[var(--wire-color-primary)]">{highlight}</span>
{/if}
</h1>
{#if description}
<p class="mt-6 text-base leading-8 text-[var(--wire-color-text-muted)] sm:text-lg">{description}</p>
{/if}
<HeroActions
actions='{[
{ label: primaryLabel, href: primaryHref, icon: primaryIcon, variant: "default", color: color },
{ label: secondaryLabel, href: secondaryHref, icon: secondaryIcon, variant: "outline", color: color }
]}'
align="left"
stackOnMobile="true"
fullWidthMobile="true"
size="lg"
color='{color}'
class="mt-8"
>
<slot name="actions"></slot>
</HeroActions>
{#if trustItems.length > 0}
<div class="mt-8 flex flex-wrap gap-x-6 gap-y-3 text-sm text-[var(--wire-color-text-muted)]">
{#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="relative min-w-0"
class:lg:order-1='reverse || visualPosition === "left"'
>
<div class="absolute -inset-8 -z-10 rounded-[2.5rem] bg-[var(--wire-color-primary-soft)] blur-2xl" aria-hidden="true"></div>
<slot name="visual"></slot>
</div>
</div>
</Section>
}
}
+40
View File
@@ -0,0 +1,40 @@
component StatsBar {
props {
items = []
columns = 4
compact = true
dividers = true
icons = true
size = "default"
color = "primary"
variant = "raised"
maxWidth = "xl"
class = ""
}
view {
<Section
spacing='{compact ? "sm" : "md"}'
variant='{variant}'
color='{color}'
maxWidth='{maxWidth}'
borderTop="true"
borderBottom="true"
class='{class}'
>
<div data-ui-component="StatsBar">
<MetricGrid
items='{items.map((item) => ({ ...item, icon: icons ? item.icon : "", variant: "minimal", size: compact ? "sm" : size }))}'
columns='{columns}'
tabletColumns="2"
mobileColumns="1"
gap='{compact ? "sm" : "md"}'
dividers='{dividers}'
size='{compact ? "sm" : size}'
color='{color}'
variant="minimal"
/>
</div>
</Section>
}
}
+77 -6
View File
@@ -1,7 +1,5 @@
component Tabs {
props {
@event change = function
@event select = function
size = "default"
color = "primary"
label = "Tabs"
@@ -10,10 +8,83 @@ component Tabs {
orientation = "horizontal"
class = ""
}
state activeValue = active || (items[0] ? (items[0].value || items[0].id || "0") : "")
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--tabs wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
<section
data-ui-component="Tabs"
class='w-full {class}'
class:flex='orientation === "vertical"'
class:items-start='orientation === "vertical"'
class:gap-6='orientation === "vertical"'
>
<div
role="tablist"
aria-label='{label}'
aria-orientation='{orientation}'
class="flex min-w-0 gap-1 overflow-x-auto rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-soft)] p-1"
class:flex-col='orientation === "vertical"'
class:w-56='orientation === "vertical"'
class:shrink-0='orientation === "vertical"'
>
{#each items as item, index}
<button
type="button"
role="tab"
id='tab-{item.value || item.id || index}'
aria-selected='{activeValue === (item.value || item.id || String(index))}'
aria-controls='panel-{item.value || item.id || index}'
tabindex='{activeValue === (item.value || item.id || String(index)) ? "0" : "-1"}'
disabled='{item.disabled || false}'
class="inline-flex min-w-max flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold outline-none transition disabled:cursor-not-allowed disabled:opacity-50"
class:bg-[var(--wire-color-surface-raised)]='activeValue === (item.value || item.id || String(index))'
class:text-[var(--wire-color-primary)]='activeValue === (item.value || item.id || String(index))'
class:shadow-sm='activeValue === (item.value || item.id || String(index))'
class:text-[var(--wire-color-text-muted)]='activeValue !== (item.value || item.id || String(index))'
class:hover:text-[var(--wire-color-text)]='activeValue !== (item.value || item.id || String(index))'
class:justify-start='orientation === "vertical"'
class:px-3='size === "sm"'
class:py-2='size === "sm"'
class:px-5='size === "lg"'
class:py-3='size === "lg"'
@click='activeValue = item.value || item.id || String(index); event.currentTarget.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { item: item, index: index, value: activeValue } })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index, value: activeValue } }))'
>
{#if item.icon}
<span class='{item.icon + " size-4"}' aria-hidden="true"></span>
{/if}
<span>{item.label || item.title}</span>
{#if item.badge}
<span class="rounded-full bg-[var(--wire-color-primary-soft)] px-2 py-0.5 text-xs text-[var(--wire-color-primary)]">{item.badge}</span>
{/if}
</button>
{/each}
</div>
<div class="min-w-0 flex-1 pt-4" class:pt-0='orientation === "vertical"'>
{#each items as item, index}
<div
role="tabpanel"
id='panel-{item.value || item.id || index}'
aria-labelledby='tab-{item.value || item.id || index}'
tabindex="0"
data-show='activeValue === (item.value || item.id || String(index))'
class="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
>
{#if item.title && item.title !== item.label}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{item.title}</h3>
{/if}
{#if item.description}
<p class="mt-2 leading-7 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
{#if item.content}
<div class="mt-4 text-[var(--wire-color-text)]">{item.content}</div>
{/if}
</div>
{/each}
<slot></slot>
</div>
</section>
}
}
+87
View File
@@ -0,0 +1,87 @@
component TextLink {
props {
@event click = function
@event focus = function
@event blur = function
label = "Learn more"
href = "#"
target = ""
rel = ""
external = false
icon = ""
iconPosition = "start"
showArrow = true
underline = false
size = "default"
color = "primary"
variant = "default"
disabled = false
class = ""
}
view {
<span
{...attrs}
data-ui-component="TextLink"
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='inline-flex min-w-0 {class}'
>
{#if disabled}
<span
aria-disabled="true"
class="inline-flex cursor-not-allowed items-center gap-2 font-semibold opacity-50"
class:text-sm='size === "sm" || size === "default"'
class:text-base='size === "md"'
class:text-lg='size === "lg"'
>
{#if icon && iconPosition === "start"}
<span class='{icon + " size-4 shrink-0"}' aria-hidden="true"></span>
{/if}
<span>{label}</span>
</span>
{:else}
<a
href='{href}'
target='{target}'
rel='{external ? (rel || "noopener noreferrer") : rel}'
class="group inline-flex items-center gap-2 rounded-md font-semibold outline-none transition 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: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: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:underline='underline'
class:underline-offset-4='underline'
class:rounded-lg='variant === "button"'
class:bg-[var(--wire-color-primary-soft)]='variant === "button" && color === "primary"'
class:px-3='variant === "button"'
class:py-2='variant === "button"'
>
{#if icon && iconPosition === "start"}
<span class='{icon + " size-4 shrink-0"}' aria-hidden="true"></span>
{/if}
<span>{label}</span>
{#if icon && iconPosition === "end"}
<span class='{icon + " size-4 shrink-0"}' aria-hidden="true"></span>
{/if}
{#if external}
<span class="icon-[lucide--external-link] size-4 shrink-0" aria-hidden="true"></span>
{:else if showArrow}
<span class="icon-[lucide--arrow-right] size-4 shrink-0 transition-transform group-hover:translate-x-1" aria-hidden="true"></span>
{/if}
</a>
{/if}
</span>
}
}
+69 -6
View File
@@ -1,6 +1,5 @@
component Timeline {
props {
@event select = function
size = "default"
color = "primary"
title = "Timeline"
@@ -9,12 +8,76 @@ component Timeline {
variant = "default"
class = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--timeline wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
<section
data-ui-component="Timeline"
aria-label='{title}'
class='w-full {class}'
>
{#if title || description}
<header class="mb-6">
{#if title}
<h3 class="text-xl font-bold text-[var(--wire-color-text)]">{title}</h3>
{/if}
{#if description}
<p class="mt-2 max-w-2xl leading-7 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
</header>
{/if}
<ol class="relative ml-4 border-l border-[var(--wire-color-border)]">
{#each items as item, index}
<li
class="relative pb-8 pl-8 last:pb-0"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
>
<span
class="absolute -left-[1.05rem] top-0 flex size-8 items-center justify-center rounded-full border-4 border-[var(--wire-color-background)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-sm"
class:bg-[var(--wire-color-success)]='item.status === "complete" || item.status === "success"'
class:bg-[var(--wire-color-warning)]='item.status === "warning" || item.status === "pending"'
class:bg-[var(--wire-color-danger)]='item.status === "danger" || item.status === "failed"'
>
<span class='{item.icon || "icon-[lucide--circle]"}' aria-hidden="true"></span>
</span>
<article
class="rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] p-5 shadow-sm"
class:border-transparent='variant === "minimal"'
class:bg-transparent='variant === "minimal"'
class:p-0='variant === "minimal"'
class:shadow-none='variant === "minimal"'
>
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<h4 class="font-bold text-[var(--wire-color-text)]">{item.title || item.label}</h4>
{#if item.subtitle}
<p class="mt-1 text-sm font-medium text-[var(--wire-color-primary)]">{item.subtitle}</p>
{/if}
</div>
{#if item.date || item.meta}
<time class="shrink-0 text-sm text-[var(--wire-color-text-muted)]">{item.date || item.meta}</time>
{/if}
</div>
{#if item.description}
<p class="mt-3 text-sm leading-6 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
{#if item.href}
<a href='{item.href}' class="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-[var(--wire-color-primary)] hover:text-[var(--wire-color-primary-hover)]">
<span>{item.actionLabel || "View details"}</span>
<span class="icon-[lucide--arrow-right] size-4" aria-hidden="true"></span>
</a>
{/if}
</article>
</li>
{:empty}
<li class="pl-8 text-sm text-[var(--wire-color-text-muted)]">No timeline items available.</li>
{/each}
</ol>
<slot></slot>
</section>
}
}
+63 -1
View File
@@ -7,7 +7,69 @@ component Typography {
maxWidth = "xl"
class = ""
}
view {
<article class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--typography wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></article>
<div
data-ui-component="Typography"
class='wire-typography text-[var(--wire-color-text)] {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-none='maxWidth === "full"'
class:text-sm='size === "sm"'
class:text-base='size === "default" || size === "md"'
class:text-lg='size === "lg"'
class:columns-1='columns === 1'
class:md:columns-2='columns === 2'
class:lg:columns-3='columns === 3'
class:gap-4='gap === "sm"'
class:gap-8='gap === "md"'
class:gap-12='gap === "lg"'
>
<slot></slot>
</div>
}
style {
.wire-typography :where(h1, h2, h3, h4) {
color: var(--wire-color-text);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.2;
break-after: avoid;
}
.wire-typography :where(h1) { font-size: clamp(2rem, 4vw, 3.5rem); margin: 0 0 1.5rem; }
.wire-typography :where(h2) { font-size: clamp(1.5rem, 3vw, 2.25rem); margin: 2.5rem 0 1rem; }
.wire-typography :where(h3) { font-size: 1.35rem; margin: 2rem 0 0.75rem; }
.wire-typography :where(p, ul, ol, blockquote, pre, table) { margin: 1rem 0; }
.wire-typography :where(p, li) { color: var(--wire-color-text-muted); line-height: 1.8; }
.wire-typography :where(a) { color: var(--wire-color-primary); font-weight: 600; text-underline-offset: 0.2em; }
.wire-typography :where(a:hover) { color: var(--wire-color-primary-hover); text-decoration: underline; }
.wire-typography :where(ul, ol) { padding-left: 1.4rem; }
.wire-typography :where(ul) { list-style: disc; }
.wire-typography :where(ol) { list-style: decimal; }
.wire-typography :where(blockquote) {
border-left: 4px solid var(--wire-color-primary);
background: var(--wire-color-primary-soft);
border-radius: 0 0.75rem 0.75rem 0;
padding: 1rem 1.25rem;
color: var(--wire-color-text);
}
.wire-typography :where(code) {
border-radius: 0.35rem;
background: var(--wire-color-surface-soft);
padding: 0.15rem 0.35rem;
font-size: 0.9em;
}
.wire-typography :where(pre) {
overflow-x: auto;
border: 1px solid var(--wire-color-border);
border-radius: 1rem;
background: var(--wire-color-surface-raised);
padding: 1rem;
}
.wire-typography :where(img) { border-radius: 1rem; }
.wire-typography :where(hr) { border-color: var(--wire-color-border); margin: 2rem 0; }
}
}
+222 -132
View File
@@ -39,221 +39,311 @@ component Card {
class = ""
}
state visible = true
state selectedNav = activeNav
state dismissed = false
functions {
function chooseNavigation(item, index) {
previousValue = selectedNav
selectedNav = item.value || item.label
$emit("navigate", {
value: selectedNav,
previousValue: previousValue,
function dispatchCardNavigation(sourceEvent, item, index, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("navigate", true, false, {
component: "Card",
item: item,
index: index
})
root.dispatchEvent(customEvent)
}
function chooseMobileNavigation(sourceEvent) {
previousValue = selectedNav
selectedNav = sourceEvent.target.value
$emit("navigate", {
value: selectedNav,
previousValue: previousValue
function dispatchCardNavigationValue(sourceEvent, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("navigate", true, false, {
component: "Card",
value: sourceEvent.currentTarget.value
})
root.dispatchEvent(customEvent)
}
function chooseAction(action, index) {
$emit("action", {
function dispatchCardHeaderAction(sourceEvent, action, index, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("action", true, false, {
component: "Card",
action: action,
index: index
})
root.dispatchEvent(customEvent)
}
function dismissCard(sourceEvent) {
panel = sourceEvent.currentTarget.closest(".wire-next__card-panel")
if (panel) {
panel.setAttribute("hidden", "")
function dismissCard(sourceEvent, root, customEvent) {
dismissed = true
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
visible = false
$emit("dismiss", {
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("dismiss", true, false, {
component: "Card",
title: title
})
root.dispatchEvent(customEvent)
}
}
view {
<section
<div
{...attrs}
class="wire-next wire-next--card {class}"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-layout="{layout}"
data-align="{align}"
data-hover="{hover}"
data-image-position="{imagePosition}"
data-group="{items.length > 0}"
aria-label="{ariaLabel || title}"
data-ui-component="Card"
data-wrn-card
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
class='wire-next wire-next--card wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}'
>
{#if items.length > 0}
<div class="wire-next__card-group">
{#each items as item}
<article class="wire-next__card-panel">
{#if item.imageSrc}
{#each items as item, itemIndex}
<article
class="wire-next__card-panel"
aria-label='{item.ariaLabel || item.title || item.label || "Card item"}'
data-index='{itemIndex}'
>
{#if item.imageSrc || item.image}
<div class="wire-next__card-media">
<img src="{item.imageSrc}" alt="{item.imageAlt || ''}" loading="lazy" />
<img
src='{item.imageSrc || item.image}'
alt='{item.imageAlt || item.alt || ""}'
loading='{item.loading || "lazy"}'
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { item: item, index: itemIndex } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { item: item, index: itemIndex } }))'
/>
</div>
{/if}
<div class="wire-next__card-body">
{#if item.title}<h3>{item.title}</h3>{/if}
{#if item.description}<p>{item.description}</p>{/if}
<div class="wire-next__card-content">
{#if item.title || item.label}
<h3>{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wire-next__card-subtitle">{item.subtitle}</p>
{/if}
{#if item.description}
<p class="wire-next__card-description">{item.description}</p>
{/if}
{#if item.actionLabel || item.href}
<a
href='{item.actionHref || item.href || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, index: itemIndex } }))'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</div>
{#if item.footer}<footer class="wire-next__card-footer">{item.footer}</footer>{/if}
</article>
{/each}
</div>
{:else if visible}
<article class="wire-next__card-panel">
{#if imagePosition === "overlay" && imageSrc}
<div class="wire-next__card-media wire-next__card-media--overlay">
{:else}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if imageSrc && imagePosition === "overlay"}
<div class="wire-next__card-overlay">
<img
src="{imageSrc}"
alt="{imageAlt}"
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
@load="$emit('load', { src: imageSrc })"
@error="$emit('error', { src: imageSrc })"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
/>
<div class="wire-next__card-overlay">
{#if title}<h3>{title}</h3>{/if}
{#if subtitle}<span class="wire-next__card-subtitle">{subtitle}</span>{/if}
{#if description}<p>{description}</p>{/if}
{#if footer}<small>{footer}</small>{/if}
</div>
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
</div>
{:else}
{#if header || headerActions.length > 0 || dismissible}
{/if}
{#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if header || title || subtitle || headerActions.length > 0 || dismissible}
<header class="wire-next__card-header">
<span>{header}</span>
<div class="wire-next__card-heading">
{#if header}
<p class="wire-next__card-eyebrow">{header}</p>
{/if}
{#if title}
<h3>{title}</h3>
{/if}
{#if subtitle}
<p class="wire-next__card-subtitle">{subtitle}</p>
{/if}
</div>
{#if headerActions.length > 0 || dismissible}
<span class="wire-next__card-header-actions">
{#each headerActions as action, index}
<div class="wire-next__card-header-actions">
{#each headerActions as headerAction, actionIndex}
<button
type="button"
aria-label="{action.ariaLabel || action.label}"
title="{action.label}"
@click="chooseAction(action, index)"
aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
@click='dispatchCardHeaderAction(event, headerAction, actionIndex)'
>
{#if action.icon}<span class="{action.icon}" aria-hidden="true"></span>{/if}
{#if !action.icon}<span>{action.label}</span>{/if}
{#if headerAction.icon}
<span class='{headerAction.icon}' aria-hidden="true"></span>
{/if}
{#if headerAction.label && !headerAction.icon}
<span>{headerAction.label}</span>
{/if}
</button>
{/each}
{#if dismissible}
<button type="button" aria-label="Close card" @click="dismissCard(event)">
<button
type="button"
aria-label="Dismiss card"
@click='dismissCard(event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</span>
</div>
{/if}
</header>
{/if}
{#if navigation.length > 0}
<nav class="wire-next__card-navigation" aria-label="{ariaLabel || title}">
<div class="wire-next__card-tabs">
{#each navigation as item, index}
<button
type="button"
data-active="{selectedNav === (item.value || item.label)}"
aria-current="{selectedNav === (item.value || item.label) ? 'page' : ''}"
@click="chooseNavigation(item, index)"
>
{item.label}
</button>
{/each}
</div>
<nav aria-label='{ariaLabel || title || "Card navigation"}' class="wire-next__card-navigation">
{#if mobileNavigation}
<select
class="wire-next__card-navigation-select"
aria-label="{ariaLabel || title}"
value="{selectedNav}"
@change="chooseMobileNavigation(event)"
aria-label='{ariaLabel || title || "Card navigation"}'
@change='dispatchCardNavigationValue(event)'
>
{#each navigation as item}
{#each navigation as navItem}
<option
value="{item.value || item.label}"
selected="{selectedNav === (item.value || item.label)}"
value='{navItem.value || navItem.href || navItem.label}'
selected='{activeNav === navItem.value || activeNav === navItem.href}'
>
{item.label}
{navItem.label}
</option>
{/each}
</select>
{/if}
<div class="wire-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
{#each navigation as navItem, navIndex}
<button
type="button"
aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
@click='dispatchCardNavigation(event, navItem, navIndex)'
>
{#if navItem.icon}
<span class='{navItem.icon}' aria-hidden="true"></span>
{/if}
<span>{navItem.label}</span>
{#if navItem.badge}
<span class="wire-next__card-tab-badge">{navItem.badge}</span>
{/if}
</button>
{/each}
</div>
</nav>
{/if}
{#if imageSrc && imagePosition !== "bottom"}
<div class="wire-next__card-media">
<img
src="{imageSrc}"
alt="{imageAlt}"
loading="lazy"
@load="$emit('load', { src: imageSrc })"
@error="$emit('error', { src: imageSrc })"
/>
</div>
{/if}
{#if alertTitle || alertDescription}
<aside class="wire-next__card-alert" role="status">
{#if alertTitle}<strong>{alertTitle}</strong>{/if}
{#if alertDescription}<span>{alertDescription}</span>{/if}
</aside>
<div class="wire-next__card-alert" role="status">
{#if alertTitle}
<strong>{alertTitle}</strong>
{/if}
{#if alertDescription}
<p>{alertDescription}</p>
{/if}
</div>
{/if}
<div
class="wire-next__card-body"
data-scrollable="{scrollable}"
style="--wire-card-max-height: {maxHeight}"
data-scrollable='{scrollable}'
style='max-height: {scrollable ? maxHeight : "none"};'
>
{#if empty}
<div class="wire-next__card-empty">
<span class="{emptyIcon}" aria-hidden="true"></span>
<strong>{emptyTitle}</strong>
<span class='{emptyIcon}' aria-hidden="true"></span>
<p>{emptyTitle}</p>
</div>
{:else}
{#if title}<h3>{title}</h3>{/if}
{#if subtitle}<span class="wire-next__card-subtitle">{subtitle}</span>{/if}
{#if description}<p>{description}</p>{/if}
<slot />
{#if actionLabel}
<a
class="wire-next__card-action"
href="{actionHref || '#'}"
@click="$emit('action', { label: actionLabel, href: actionHref })"
>
{actionLabel}<span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
</a>
{#if description}
<p class="wire-next__card-description">{description}</p>
{/if}
<slot></slot>
{/if}
</div>
{#if footer}<footer class="wire-next__card-footer">{footer}</footer>{/if}
{#if imageSrc && imagePosition === "bottom"}
<div class="wire-next__card-media">
<img
src="{imageSrc}"
alt="{imageAlt}"
loading="lazy"
@load="$emit('load', { src: imageSrc })"
@error="$emit('error', { src: imageSrc })"
/>
</div>
{#if footer || actionLabel}
<footer class="wire-next__card-footer">
{#if footer}
<span>{footer}</span>
{/if}
{#if actionLabel}
<a
href='{actionHref || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</footer>
{/if}
</div>
{#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
/>
</div>
{/if}
</article>
{/if}
</section>
</div>
}
}
+34 -1
View File
@@ -7,7 +7,40 @@ component Container {
maxWidth = "xl"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--container wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
data-ui-component="Container"
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
data-max-width='{maxWidth}'
class='mx-auto w-full {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl" || maxWidth === "wide"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:px-4='size === "compact" || size === "default"'
class:px-5='size === "comfortable"'
class:px-6='size === "spacious"'
class:sm:px-6='size !== "compact"'
class:lg:px-8='size === "default" || size === "comfortable" || size === "spacious"'
class:grid='columns > 1'
class:grid-cols-1='columns > 1'
class:sm:grid-cols-2='columns === 2 || columns === 3 || columns === 4 || columns === 5 || columns === 6'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:lg:grid-cols-5='columns === 5'
class:lg:grid-cols-6='columns === 6'
class:gap-2='gap === "xs"'
class:gap-3='gap === "sm"'
class:gap-5='gap === "md"'
class:gap-8='gap === "lg"'
class:gap-10='gap === "xl"'
>
<slot></slot>
</div>
}
}
+58 -1
View File
@@ -6,7 +6,64 @@ component Divider {
orientation = "horizontal"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--divider wire-next--divider-{orientation} {class}" role="separator" aria-orientation="{orientation}">{#if label}<span>{label}</span>{/if}</div>
<div
{...attrs}
data-ui-component="Divider"
data-size='{size}'
data-color='{color}'
data-orientation='{orientation}'
class='w-full {class}'
class:h-full='orientation === "vertical"'
class:w-auto='orientation === "vertical"'
>
{#if orientation === "vertical"}
<div
role="separator"
aria-orientation="vertical"
class="mx-3 h-full min-h-6 border-l border-[var(--wire-color-border)]"
class:border-l-2='size === "lg"'
class:border-[var(--wire-color-primary)]='color === "primary"'
class:border-[var(--wire-color-secondary)]='color === "secondary"'
class:border-[var(--wire-color-success)]='color === "success"'
class:border-[var(--wire-color-warning)]='color === "warning"'
class:border-[var(--wire-color-danger)]='color === "danger"'
></div>
{:else}
<div
role="separator"
aria-orientation="horizontal"
class="flex w-full items-center gap-4"
>
<span
class="h-px flex-1 bg-[var(--wire-color-border)]"
class:h-0.5='size === "lg"'
class:bg-[var(--wire-color-primary)]='color === "primary" && label !== ""'
class:bg-[var(--wire-color-secondary)]='color === "secondary" && label !== ""'
class:bg-[var(--wire-color-success)]='color === "success" && label !== ""'
class:bg-[var(--wire-color-warning)]='color === "warning" && label !== ""'
class:bg-[var(--wire-color-danger)]='color === "danger" && label !== ""'
aria-hidden="true"
></span>
{#if label}
<span class="shrink-0 text-xs font-bold uppercase tracking-[0.16em] text-[var(--wire-color-text-muted)]">
{label}
</span>
<span
class="h-px flex-1 bg-[var(--wire-color-border)]"
class:h-0.5='size === "lg"'
class:bg-[var(--wire-color-primary)]='color === "primary"'
class:bg-[var(--wire-color-secondary)]='color === "secondary"'
class:bg-[var(--wire-color-success)]='color === "success"'
class:bg-[var(--wire-color-warning)]='color === "warning"'
class:bg-[var(--wire-color-danger)]='color === "danger"'
aria-hidden="true"
></span>
{/if}
</div>
{/if}
</div>
}
}
+28 -1
View File
@@ -7,7 +7,34 @@ component Grid {
maxWidth = "xl"
class = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--grid wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
<div
data-ui-component="Grid"
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:sm:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:lg:grid-cols-5='columns === 5'
class:lg:grid-cols-6='columns === 6'
class:gap-2='gap === "xs"'
class:gap-3='gap === "sm"'
class:gap-5='gap === "md"'
class:gap-8='gap === "lg"'
class:gap-10='gap === "xl"'
class:items-start='size === "compact"'
class:items-stretch='size !== "compact"'
>
<slot></slot>
</div>
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -0,0 +1,172 @@
import { describe, expect, test } from "bun:test";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
const componentsDir = join(import.meta.dir, "..", "components");
const replacementComponents = [
"Container",
"Grid",
"Columns",
"Card",
"Link",
"Image",
"SearchBox",
"Map",
"List",
"Marquee",
"Tabs",
"Timeline",
"Footer",
"Typography",
"Divider",
] as const;
const newComponents = [
"Section",
"SectionHeader",
"MarketingSectionHeader",
"PageHeader",
"TextLink",
"FeatureGrid",
"FeatureCard",
"FeatureIconCard",
"Hero",
"SplitHero",
"HeroActions",
"AnnouncementBar",
"MetricCard",
"MetricGrid",
"StatsBar",
"CTASection",
"BackToTop",
"PublicPageShell",
] as const;
const allComponents = [...replacementComponents, ...newComponents];
function openingTags(source: string): string[] {
const tags: string[] = [];
let index = 0;
while (index < source.length) {
const start = source.indexOf("<", index);
if (start === -1) break;
let cursor = start + 1;
let quote: "'" | '"' | null = null;
while (cursor < source.length) {
const char = source[cursor];
if (quote) {
if (char === quote) quote = null;
} else if (char === "'" || char === '"') {
quote = char;
} else if (char === ">") {
tags.push(source.slice(start, cursor + 1));
cursor += 1;
break;
}
cursor += 1;
}
index = cursor;
}
return tags;
}
describe("@wrnexus/ui redesign component pack", () => {
test("contains every replacement and new component", async () => {
const files = new Set(await readdir(componentsDir));
for (const name of allComponents) {
expect(files.has(`${name}.wrn`)).toBe(true);
}
});
for (const name of allComponents) {
test(`${name} has a valid component shell`, async () => {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
expect(source).toContain(`component ${name} {`);
expect(source).toContain("props {");
expect(source).toContain("view {");
expect(source).toContain(`data-ui-component="${name}"`);
expect(source).not.toContain("React");
expect(source).not.toContain("useState");
expect(source).not.toContain("<script");
});
}
test("opening tags do not contain duplicate class attributes", async () => {
for (const name of allComponents) {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
for (const tag of openingTags(source)) {
const plainClassAttributes = tag.match(/(?<![:\\w-])class\\s*=/g) ?? [];
expect(plainClassAttributes.length).toBeLessThanOrEqual(1);
}
}
});
test("dynamic attribute expressions stay single quoted", async () => {
for (const name of allComponents) {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
expect(source).not.toMatch(/=\\"\{[^\n]*\}\\"/);
}
});
test("existing public defaults remain release compatible", async () => {
const expectations: Record<string, string[]> = {
Container: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Grid: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Columns: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Link: ['label = "Link"', 'href = "#"', "external = false"],
Image: ['src = ""', 'alt = ""', 'loading = "lazy"', "rounded = false"],
SearchBox: [
'label = "Search Box"',
'type = "search"',
"disabled = false",
"required = false",
],
Map: ['title = "Map"', "items = []", 'variant = "default"'],
List: ['title = "List"', "items = []", 'variant = "default"'],
Marquee: ['title = "Marquee"', "items = []", 'variant = "default"'],
Tabs: ['label = "Tabs"', "items = []", 'active = ""', 'orientation = "horizontal"'],
Timeline: ['title = "Timeline"', "items = []", 'variant = "default"'],
Footer: ['label = "Footer navigation"', "items = []", "columns = 3", 'maxWidth = "compact"'],
Typography: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Divider: ['label = ""', 'orientation = "horizontal"'],
};
for (const [name, tokens] of Object.entries(expectations)) {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
for (const token of tokens) expect(source).toContain(token);
}
});
});
+1 -1
View File
@@ -22,7 +22,7 @@ test("generated component reference documents every bundled component and its pr
readFileSync(join(uiComponentsDir(), "..", "component-catalog.json"), "utf8"),
) as { components: Array<{ name: string }> };
expect(reference.count).toBe(uiComponentNames().length);
expect(reference.count).toBe(90);
expect(reference.count).toBe(108);
expect(reference.components.map((component) => component.name)).toEqual(
expect.arrayContaining(uiComponentNames()),
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
"version": "0.5.11",
"version": "0.5.12",
"private": true,
"type": "module",
"main": "src/index.ts",