release: WRNexusJS 0.5.10
This commit is contained in:
+57
-10
@@ -1,6 +1,6 @@
|
||||
# WRNexus UI component reference
|
||||
|
||||
This reference is generated from the 85 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
|
||||
This reference is generated from the 90 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
|
||||
|
||||
## Advanced-forms
|
||||
|
||||
@@ -330,15 +330,35 @@ Theme-aware, responsive tree view component.
|
||||
- Slots: `default`
|
||||
- Events: `select`, `toggle`, `expand`, `collapse`
|
||||
|
||||
## Blocks
|
||||
|
||||
### PortalDashboard
|
||||
|
||||
Responsive portal dashboard with metrics, actions, tasks, and updates.
|
||||
|
||||
- Mount: `data-component="PortalDashboard"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Overview"`, `eyebrowKey: string = ""`, `title: string = "Dashboard"`, `titleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `userName: string = ""`, `metrics: string = []`, `actions: string = []`, `updates: string = []`, `tasks: string = []`, `class: string = ""`
|
||||
- Slots: `hero-action`
|
||||
- Events: `action`, `navigate`
|
||||
|
||||
## Forms
|
||||
|
||||
### AuthForm
|
||||
|
||||
Reusable authentication form for sign-in, registration, recovery, reset, and MFA.
|
||||
|
||||
- Mount: `data-component="AuthForm"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `mode: string = "sign-in"`, `action: string = "/api/auth/login"`, `method: string = "post"`, `title: string = "Sign in"`, `description: string = ""`, `returnTo: string = ""`, `schema: string = ""`, `showRemember: boolean = true`, `showName: boolean = true`, `submitLabel: string = "Continue"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `submit`, `change`, `input`, `focus`, `blur`
|
||||
|
||||
### Checkbox
|
||||
|
||||
Theme-aware, responsive checkbox component.
|
||||
|
||||
- Mount: `data-component="Checkbox"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Checkbox"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `values: string = []`, `options: string = []`, `checked: boolean = false`, `indeterminate: boolean = false`, `orientation: string = "vertical"`, `card: boolean = false`, `rightAligned: boolean = false`, `list: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Slots: `default`
|
||||
- Events: `input`, `change`, `focus`, `blur`, `invalid`
|
||||
|
||||
### ColorPicker
|
||||
@@ -543,6 +563,15 @@ Theme-aware, responsive wysiwyg editor component.
|
||||
|
||||
## Layout
|
||||
|
||||
### AuthSplitLayout
|
||||
|
||||
Responsive 50/50 authentication layout with content and form regions.
|
||||
|
||||
- Mount: `data-component="AuthSplitLayout"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Secure identity"`, `title: string = "Welcome back"`, `description: string = ""`, `brand: string = "Police Management System"`, `features: string = []`, `class: string = ""`
|
||||
- Slots: `aside-extra`, `form`
|
||||
- Events: None
|
||||
|
||||
### Columns
|
||||
|
||||
Theme-aware, responsive columns component.
|
||||
@@ -644,6 +673,15 @@ Theme-aware, responsive breadcrumb component.
|
||||
- Slots: `default`
|
||||
- Events: `navigate`, `click`
|
||||
|
||||
### Footer
|
||||
|
||||
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`
|
||||
- Events: `select`, `action`
|
||||
|
||||
### MegaMenu
|
||||
|
||||
Theme-aware, responsive mega menu component.
|
||||
@@ -667,9 +705,9 @@ Theme-aware, responsive nav component.
|
||||
Theme-aware, responsive navbar component.
|
||||
|
||||
- Mount: `data-component="Navbar"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Navbar"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `toggle`, `open`, `close`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: string = {}`, `items: string = []`, `actions: string = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
|
||||
- Slots: `topbar`, `actions`
|
||||
- Events: `toggle`, `open`, `close`, `select`, `action`
|
||||
|
||||
### Pagination
|
||||
|
||||
@@ -680,6 +718,15 @@ Theme-aware, responsive pagination component.
|
||||
- Slots: `default`
|
||||
- Events: `change`, `previous`, `next`
|
||||
|
||||
### PreferenceSwitcher
|
||||
|
||||
Accessible theme, accent color, and language preference controls.
|
||||
|
||||
- Mount: `data-component="PreferenceSwitcher"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: `theme`, `color`, `language`
|
||||
|
||||
### Scrollspy
|
||||
|
||||
Theme-aware, responsive scrollspy component.
|
||||
@@ -694,9 +741,9 @@ Theme-aware, responsive scrollspy component.
|
||||
Theme-aware, responsive sidebar component.
|
||||
|
||||
- Mount: `data-component="Sidebar"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Sidebar"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Sidebar"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mobileLabel: string = "Open navigation"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `toggle`, `open`, `close`
|
||||
- Events: `toggle`, `open`, `close`, `select`
|
||||
|
||||
### Stepper
|
||||
|
||||
@@ -741,9 +788,9 @@ Theme-aware, responsive drawer component.
|
||||
Theme-aware, responsive dropdown component.
|
||||
|
||||
- Mount: `data-component="Dropdown"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Dropdown"`, `description: string = ""`, `open: boolean = false`, `placement: string = "bottom"`, `closeLabel: string = "Close"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `open`, `close`, `select`, `change`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Open menu"`, `items: string = []`, `placement: string = "end"`, `class: string = ""`
|
||||
- Slots: `trigger`, `header`, `footer`
|
||||
- Events: `toggle`, `open`, `close`, `select`
|
||||
|
||||
### Modal
|
||||
|
||||
|
||||
@@ -191,6 +191,11 @@
|
||||
"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",
|
||||
@@ -429,6 +434,26 @@
|
||||
"name": "WysiwygEditor",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive wysiwyg editor component."
|
||||
},
|
||||
{
|
||||
"name": "PreferenceSwitcher",
|
||||
"category": "navigation",
|
||||
"purpose": "Accessible theme, accent color, and language preference controls."
|
||||
},
|
||||
{
|
||||
"name": "PortalDashboard",
|
||||
"category": "blocks",
|
||||
"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": "AuthForm",
|
||||
"category": "forms",
|
||||
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"generatedFrom": "packages/ui/components/*.wrn",
|
||||
"count": 85,
|
||||
"count": 90,
|
||||
"components": [
|
||||
{
|
||||
"name": "Accordion",
|
||||
@@ -675,6 +675,154 @@
|
||||
"events": ["dismiss", "action"],
|
||||
"source": "components/alert.wrn"
|
||||
},
|
||||
{
|
||||
"name": "AuthForm",
|
||||
"mount": "AuthForm",
|
||||
"category": "forms",
|
||||
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA.",
|
||||
"props": [
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\""
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"primary\""
|
||||
},
|
||||
{
|
||||
"name": "mode",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"sign-in\""
|
||||
},
|
||||
{
|
||||
"name": "action",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"/api/auth/login\""
|
||||
},
|
||||
{
|
||||
"name": "method",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"post\""
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Sign in\""
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "returnTo",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "schema",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "showRemember",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "true"
|
||||
},
|
||||
{
|
||||
"name": "showName",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "true"
|
||||
},
|
||||
{
|
||||
"name": "submitLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Continue\""
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": ["default"],
|
||||
"events": ["submit", "change", "input", "focus", "blur"],
|
||||
"source": "components/AuthForm.wrn"
|
||||
},
|
||||
{
|
||||
"name": "AuthSplitLayout",
|
||||
"mount": "AuthSplitLayout",
|
||||
"category": "layout",
|
||||
"purpose": "Responsive 50/50 authentication layout with content and form regions.",
|
||||
"props": [
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\""
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"primary\""
|
||||
},
|
||||
{
|
||||
"name": "eyebrow",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Secure identity\""
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Welcome back\""
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "brand",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Police Management System\""
|
||||
},
|
||||
{
|
||||
"name": "features",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": ["aside-extra", "form"],
|
||||
"events": [],
|
||||
"source": "components/AuthSplitLayout.wrn"
|
||||
},
|
||||
{
|
||||
"name": "Avatar",
|
||||
"mount": "Avatar",
|
||||
@@ -2062,7 +2210,7 @@
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": [],
|
||||
"slots": ["default"],
|
||||
"events": ["input", "change", "focus", "blur", "invalid"],
|
||||
"source": "components/Checkbox.wrn"
|
||||
},
|
||||
@@ -3436,34 +3584,22 @@
|
||||
"default": "\"primary\""
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"name": "label",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Dropdown\""
|
||||
"default": "\"Open menu\""
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"name": "items",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "open",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false"
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "placement",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"bottom\""
|
||||
},
|
||||
{
|
||||
"name": "closeLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Close\""
|
||||
"default": "\"end\""
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
@@ -3472,8 +3608,8 @@
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": ["default"],
|
||||
"events": ["open", "close", "select", "change"],
|
||||
"slots": ["trigger", "header", "footer"],
|
||||
"events": ["toggle", "open", "close", "select"],
|
||||
"source": "components/Dropdown.wrn"
|
||||
},
|
||||
{
|
||||
@@ -3755,6 +3891,65 @@
|
||||
"events": ["cancel", "retry", "complete"],
|
||||
"source": "components/FileUploadProgress.wrn"
|
||||
},
|
||||
{
|
||||
"name": "Footer",
|
||||
"mount": "Footer",
|
||||
"category": "navigation",
|
||||
"purpose": "Responsive application footer with structured links and pre/post content slots.",
|
||||
"props": [
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\""
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"primary\""
|
||||
},
|
||||
{
|
||||
"name": "label",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Footer navigation\""
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "columns",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"default": "3"
|
||||
},
|
||||
{
|
||||
"name": "maxWidth",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"compact\""
|
||||
},
|
||||
{
|
||||
"name": "copyright",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": ["pre-footer", "post-footer", "copyright-left", "copyright-right"],
|
||||
"events": ["select", "action"],
|
||||
"source": "components/Footer.wrn"
|
||||
},
|
||||
{
|
||||
"name": "Grid",
|
||||
"mount": "Grid",
|
||||
@@ -5039,7 +5234,19 @@
|
||||
"name": "label",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Navbar\""
|
||||
"default": "\"Primary navigation\""
|
||||
},
|
||||
{
|
||||
"name": "topbarLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Utility navigation\""
|
||||
},
|
||||
{
|
||||
"name": "brand",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "{}"
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
@@ -5047,6 +5254,12 @@
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "actions",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"type": "string",
|
||||
@@ -5054,10 +5267,28 @@
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "orientation",
|
||||
"name": "sticky",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false"
|
||||
},
|
||||
{
|
||||
"name": "openOnHover",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false"
|
||||
},
|
||||
{
|
||||
"name": "maxWidth",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"horizontal\""
|
||||
"default": "\"full\""
|
||||
},
|
||||
{
|
||||
"name": "mobileLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Toggle navigation\""
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
@@ -5066,8 +5297,8 @@
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": ["default"],
|
||||
"events": ["toggle", "open", "close"],
|
||||
"slots": ["topbar", "actions"],
|
||||
"events": ["toggle", "open", "close", "select", "action"],
|
||||
"source": "components/Navbar.wrn"
|
||||
},
|
||||
{
|
||||
@@ -5349,6 +5580,166 @@
|
||||
"events": ["open", "close"],
|
||||
"source": "components/Popover.wrn"
|
||||
},
|
||||
{
|
||||
"name": "PortalDashboard",
|
||||
"mount": "PortalDashboard",
|
||||
"category": "blocks",
|
||||
"purpose": "Responsive portal dashboard with metrics, actions, tasks, and updates.",
|
||||
"props": [
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\""
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"primary\""
|
||||
},
|
||||
{
|
||||
"name": "eyebrow",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Overview\""
|
||||
},
|
||||
{
|
||||
"name": "eyebrowKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Dashboard\""
|
||||
},
|
||||
{
|
||||
"name": "titleKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "descriptionKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "userName",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "metrics",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "actions",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "tasks",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "[]"
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": ["hero-action"],
|
||||
"events": ["action", "navigate"],
|
||||
"source": "components/PortalDashboard.wrn"
|
||||
},
|
||||
{
|
||||
"name": "PreferenceSwitcher",
|
||||
"mount": "PreferenceSwitcher",
|
||||
"category": "navigation",
|
||||
"purpose": "Accessible theme, accent color, and language preference controls.",
|
||||
"props": [
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\""
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"primary\""
|
||||
},
|
||||
{
|
||||
"name": "themeLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Theme\""
|
||||
},
|
||||
{
|
||||
"name": "colorLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Accent color\""
|
||||
},
|
||||
{
|
||||
"name": "languageLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Language\""
|
||||
},
|
||||
{
|
||||
"name": "languages",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "["
|
||||
},
|
||||
{
|
||||
"name": "colors",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "["
|
||||
},
|
||||
{
|
||||
"name": "compact",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "true"
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": [],
|
||||
"events": ["theme", "color", "language"],
|
||||
"source": "components/PreferenceSwitcher.wrn"
|
||||
},
|
||||
{
|
||||
"name": "Progress",
|
||||
"mount": "Progress",
|
||||
@@ -6110,6 +6501,12 @@
|
||||
"required": false,
|
||||
"default": "\"horizontal\""
|
||||
},
|
||||
{
|
||||
"name": "mobileLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Open navigation\""
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
@@ -6118,7 +6515,7 @@
|
||||
}
|
||||
],
|
||||
"slots": ["default"],
|
||||
"events": ["toggle", "open", "close"],
|
||||
"events": ["toggle", "open", "close", "select"],
|
||||
"source": "components/Sidebar.wrn"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
component AuthForm {
|
||||
props {
|
||||
@event submit = function
|
||||
@event change = function
|
||||
@event input = function
|
||||
@event focus = function
|
||||
@event blur = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
mode = "sign-in"
|
||||
action = "/api/auth/login"
|
||||
method = "post"
|
||||
title = "Sign in"
|
||||
description = ""
|
||||
returnTo = ""
|
||||
schema = ""
|
||||
showRemember = true
|
||||
showName = true
|
||||
submitLabel = "Continue"
|
||||
class = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
function schemaName() {
|
||||
if (schema) return schema
|
||||
if (mode === "sign-in") return "auth-login"
|
||||
if (mode === "register") return "auth-register"
|
||||
if (mode === "recover") return "auth-password-request"
|
||||
if (mode === "reset") return "auth-password-reset"
|
||||
if (mode === "mfa") return "auth-mfa"
|
||||
return "auth-empty"
|
||||
}
|
||||
|
||||
function submitForm(event) {
|
||||
$emit("submit", { event: event, mode: mode, action: action })
|
||||
}
|
||||
function fieldEvent(type, event) {
|
||||
const detail = event.detail || {}
|
||||
$emit(type, { event: event, name: detail.name || "", value: detail.value || "" })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-auth-form wire-next--color-{color} wire-next--size-{size} {class}">
|
||||
<header>
|
||||
<span class="wire-auth-form__icon icon-[lucide--fingerprint]" aria-hidden="true"></span>
|
||||
<div><h2>{title}</h2>{#if description}<p>{description}</p>{/if}</div>
|
||||
</header>
|
||||
<form
|
||||
action="{action}"
|
||||
method="{method}"
|
||||
data-schema="{schemaName()}"
|
||||
data-wrnexus-runtime="auth"
|
||||
novalidate="true"
|
||||
@submit="submitForm($event)"
|
||||
>
|
||||
{#if returnTo}<input type="hidden" name="returnTo" value="{returnTo}" />{/if}
|
||||
{#if mode === "register" && showName}
|
||||
<Input label="Full name" name="displayName" autocomplete="name" placeholder="Enter your full name" required="true" icon="icon-[lucide--user-round]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
|
||||
{/if}
|
||||
{#if mode === "sign-in" || mode === "register" || mode === "recover"}
|
||||
<Input label="{mode === 'recover' ? 'Registered email or mobile' : 'Email, mobile, or username'}" name="{mode === 'recover' ? 'identifier' : mode === 'register' ? 'email' : 'identifier'}" type="{mode === 'register' ? 'email' : 'text'}" autocomplete="{mode === 'register' ? 'email' : 'username'}" placeholder="{mode === 'recover' ? 'Enter your registered identity' : 'Enter your identity'}" required="true" icon="icon-[lucide--at-sign]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
|
||||
{/if}
|
||||
{#if mode === "sign-in" || mode === "register" || mode === "reset"}
|
||||
<Input label="{mode === 'reset' ? 'New password' : 'Password'}" name="password" type="password" autocomplete="{mode === 'sign-in' ? 'current-password' : 'new-password'}" placeholder="Enter your password" required="true" icon="icon-[lucide--lock-keyhole]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
|
||||
{/if}
|
||||
{#if mode === "register" || mode === "reset"}
|
||||
<Input label="Confirm password" name="confirmPassword" type="password" autocomplete="new-password" placeholder="Enter the password again" required="true" icon="icon-[lucide--shield-check]" iconPosition="start" @change="fieldEvent('change', $event)" @input="fieldEvent('input', $event)" @focus="fieldEvent('focus', $event)" @blur="fieldEvent('blur', $event)" />
|
||||
{/if}
|
||||
{#if mode === "mfa"}
|
||||
<PinInput label="Verification code" name="code" length={6} inputMode="numeric" />
|
||||
{/if}
|
||||
{#if mode === "sign-in" && showRemember}
|
||||
<div class="wire-auth-form__options">
|
||||
<Checkbox label="Remember this device" name="rememberDevice" value="on" />
|
||||
<a href="/forgot-password">Forgot password?</a>
|
||||
</div>
|
||||
{/if}
|
||||
<Button type="submit" label="{submitLabel}" variant="default" color="{color}" size="lg" fullWidth="true" icon="icon-[lucide--arrow-right]" iconPosition="end" class="wire-auth-form__submit" />
|
||||
</form>
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
component AuthSplitLayout {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
eyebrow = "Secure identity"
|
||||
title = "Welcome back"
|
||||
description = ""
|
||||
brand = "Police Management System"
|
||||
features = []
|
||||
class = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<main
|
||||
class="wire-auth-split wire-next--color-{color} wire-next--size-{size} {class}"
|
||||
>
|
||||
<section
|
||||
class="wire-auth-split__content"
|
||||
>
|
||||
<div
|
||||
class="wire-auth-split__brand"
|
||||
>
|
||||
<span
|
||||
class="icon-[lucide--shield-check]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
<span><strong>{brand}</strong><small>Unified identity and access</small></span>
|
||||
</div>
|
||||
<div
|
||||
class="wire-auth-split__message"
|
||||
>
|
||||
<span
|
||||
class="wire-auth-split__eyebrow"
|
||||
>
|
||||
{eyebrow}
|
||||
</span>
|
||||
<h1>{title}</h1>
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
<div
|
||||
class="wire-auth-split__features"
|
||||
>
|
||||
{#each features as item}
|
||||
<article>
|
||||
<span
|
||||
class="{item.icon || 'icon-[lucide--check-circle-2]'}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
<span><strong>{item.label}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
<slot
|
||||
name="aside-extra"
|
||||
/>
|
||||
</div>
|
||||
<small
|
||||
class="wire-auth-split__legal"
|
||||
>
|
||||
Authorised access only · Activity may be monitored and audited.
|
||||
</small>
|
||||
</section>
|
||||
<section
|
||||
class="wire-auth-split__form"
|
||||
>
|
||||
<div
|
||||
class="wire-auth-split__form-inner"
|
||||
>
|
||||
<slot
|
||||
name="form"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,9 @@ component AvatarGroup {
|
||||
role="group"
|
||||
aria-label="Avatar group"
|
||||
>
|
||||
<div class="wire-next__avatar-group-members">
|
||||
<div
|
||||
class="wire-next__avatar-group-members"
|
||||
>
|
||||
{#each visibleMembers() as item}
|
||||
<span
|
||||
class="wire-next__avatar-group-member"
|
||||
@@ -64,18 +66,35 @@ component AvatarGroup {
|
||||
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
|
||||
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
|
||||
>
|
||||
<span class="wire-next__avatar-group-avatar">
|
||||
<span
|
||||
class="wire-next__avatar-group-avatar"
|
||||
>
|
||||
{#if item.src}
|
||||
<img src="{item.src}" alt="{item.alt || item.name || ''}" loading="lazy" />
|
||||
<img
|
||||
src="{item.src}"
|
||||
alt="{item.alt || item.name || ''}"
|
||||
loading="lazy"
|
||||
/>
|
||||
{:else if item.initials}
|
||||
<span aria-hidden="true">{item.initials}</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
{item.initials}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--user-round]" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon-[lucide--user-round]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if showTooltips && (item.tooltip || item.name)}
|
||||
<span class="wire-next__avatar-group-tooltip" role="tooltip">
|
||||
<span
|
||||
class="wire-next__avatar-group-tooltip"
|
||||
role="tooltip"
|
||||
>
|
||||
{item.tooltip || item.name}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -83,7 +102,9 @@ component AvatarGroup {
|
||||
{/each}
|
||||
|
||||
{#if hiddenMembers().length > 0}
|
||||
<span class="wire-next__avatar-group-overflow">
|
||||
<span
|
||||
class="wire-next__avatar-group-overflow"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__avatar-group-overflow-button"
|
||||
@@ -101,10 +122,19 @@ component AvatarGroup {
|
||||
aria-hidden="{overflowOpen ? 'false' : 'true'}"
|
||||
>
|
||||
{#each hiddenMembers() as item}
|
||||
<span class="wire-next__avatar-group-menu-item" role="menuitem">
|
||||
<span class="wire-next__avatar-group-menu-avatar">
|
||||
<span
|
||||
class="wire-next__avatar-group-menu-item"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
class="wire-next__avatar-group-menu-avatar"
|
||||
>
|
||||
{#if item.src}
|
||||
<img src="{item.src}" alt="" loading="lazy" />
|
||||
<img
|
||||
src="{item.src}"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
{:else}
|
||||
<span>{item.initials || '?'}</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,23 +1,53 @@
|
||||
component Dropdown {
|
||||
props {
|
||||
@event toggle = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event select = function
|
||||
@event change = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Dropdown"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
label = "Open menu"
|
||||
items = []
|
||||
placement = "end"
|
||||
class = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
function toggleMenu(event) {
|
||||
$emit("toggle", { open: event.currentTarget.open })
|
||||
$emit(event.currentTarget.open ? "open" : "close", { source: "trigger" })
|
||||
}
|
||||
|
||||
function selectItem(item) {
|
||||
$emit("select", { item: item, value: item.value || "" })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--dropdown wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
</div>
|
||||
<details class="wire-dropdown wire-dropdown--{placement} wire-next--color-{color} wire-next--size-{size} {class}" data-wrn-dropdown @toggle="toggleMenu($event)">
|
||||
<summary aria-label="{label}">
|
||||
<slot name="trigger" />
|
||||
<span class="wire-dropdown__chevron icon-[lucide--chevron-down]" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="wire-dropdown__panel" role="menu" aria-label="{label}">
|
||||
<slot name="header" />
|
||||
{#each items as item}
|
||||
{#if item.type === "divider"}
|
||||
<hr />
|
||||
{:else}
|
||||
{#if item.type === "header"}
|
||||
<span class="wire-dropdown__heading">{item.label}</span>
|
||||
{:else}
|
||||
<a class="wire-dropdown__item {item.danger ? 'wire-dropdown__item--danger' : ''}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" role="menuitem" @click="selectItem(item)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
|
||||
{#if item.external}<span class="icon-[lucide--arrow-up-right]" aria-hidden="true"></span>{/if}
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
component Footer {
|
||||
props {
|
||||
@event select = function
|
||||
@event action = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Footer navigation"
|
||||
items = []
|
||||
columns = 3
|
||||
maxWidth = "compact"
|
||||
copyright = ""
|
||||
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>
|
||||
|
||||
<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>
|
||||
{/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}
|
||||
</nav>
|
||||
|
||||
<div class="wire-footer__post">
|
||||
<slot name="post-footer" />
|
||||
</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}
|
||||
</footer>
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,138 @@ component Navbar {
|
||||
@event toggle = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event select = function
|
||||
@event action = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Navbar"
|
||||
label = "Primary navigation"
|
||||
topbarLabel = "Utility navigation"
|
||||
brand = {}
|
||||
items = []
|
||||
actions = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
sticky = false
|
||||
openOnHover = false
|
||||
maxWidth = "full"
|
||||
mobileLabel = "Toggle navigation"
|
||||
class = ""
|
||||
}
|
||||
|
||||
state mobileOpen = false
|
||||
|
||||
functions {
|
||||
function toggleNavigation() {
|
||||
mobileOpen = !mobileOpen
|
||||
$emit("toggle", { open: mobileOpen })
|
||||
$emit(mobileOpen ? "open" : "close", { source: "mobile" })
|
||||
}
|
||||
|
||||
function selectItem(item, level) {
|
||||
mobileOpen = false
|
||||
$emit("select", { item: item, value: item.value || "", level: level })
|
||||
}
|
||||
|
||||
function selectAction(item) {
|
||||
mobileOpen = false
|
||||
$emit("action", { item: item, value: item.value || "" })
|
||||
}
|
||||
|
||||
function isItemActive(item) {
|
||||
if (!item) return false
|
||||
if (item.value && item.value === active) return true
|
||||
if (!item.children || !item.children.length) return false
|
||||
return item.children.some(function (child) {
|
||||
return isItemActive(child)
|
||||
})
|
||||
}
|
||||
|
||||
function toggleDropdown(event, item) {
|
||||
$emit(event.currentTarget.open ? "open" : "close", {
|
||||
source: "menu",
|
||||
item: item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--navbar 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>
|
||||
<header class="wire-navbar wire-navbar--width-{maxWidth} wire-next--color-{color} wire-next--size-{size} {sticky ? 'wire-navbar--sticky' : ''} {class}" data-wrn-navbar data-open-on-hover="{openOnHover}">
|
||||
<div class="wire-navbar__topbar" aria-label="{topbarLabel}">
|
||||
<slot name="topbar" />
|
||||
</div>
|
||||
|
||||
<div class="wire-navbar__main">
|
||||
<a class="wire-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
|
||||
{#if brand.logo}
|
||||
<img class="wire-navbar__brand-logo" src="{brand.logo}" alt="{brand.logoAlt || brand.label || ''}" width="{brand.logoWidth || ''}" height="{brand.logoHeight || ''}" />
|
||||
{/if}
|
||||
{#if brand.icon}
|
||||
<span class="wire-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if brand.label || brand.description}
|
||||
<span class="wire-navbar__brand-copy">
|
||||
{#if brand.label}<strong>{brand.label}</strong>{/if}
|
||||
{#if brand.description}<small>{brand.description}</small>{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<button type="button" class="wire-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<div class="wire-navbar__collapse {mobileOpen ? 'is-open' : ''}">
|
||||
<nav class="wire-navbar__menus" aria-label="{label}">
|
||||
{#each items as item}
|
||||
{#if item.children && item.children.length}
|
||||
<details class="wire-navbar__dropdown wire-navbar__dropdown--{item.type || 'dropdown'}" name="wire-navbar-menu" @toggle="toggleDropdown($event, item)">
|
||||
<summary aria-current="{isItemActive(item) ? 'page' : ''}">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
<span class="wire-navbar__chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="wire-navbar__panel wire-navbar__panel--columns-{item.columns || 1}">
|
||||
{#if item.description}<p class="wire-navbar__panel-intro">{item.description}</p>{/if}
|
||||
{#each item.children as child}
|
||||
<div class="wire-navbar__group">
|
||||
{#if child.children && child.children.length}
|
||||
{#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
|
||||
{#if child.description}<small>{child.description}</small>{/if}
|
||||
{#each child.children as nested}
|
||||
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : ''}" @click="selectItem(nested, 3)">
|
||||
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong>{nested.label}</strong>{#if nested.description}<small>{nested.description}</small>{/if}</span>
|
||||
</a>
|
||||
{/each}
|
||||
{:else}
|
||||
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
|
||||
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong>{child.label}</strong>{#if child.description}<small>{child.description}</small>{/if}</span>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a class="wire-navbar__menu-link" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="wire-navbar__actions">
|
||||
{#each actions as item}
|
||||
<a class="wire-navbar__action wire-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
component PortalDashboard {
|
||||
props {
|
||||
@event action = function
|
||||
@event navigate = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
eyebrow = "Overview"
|
||||
eyebrowKey = ""
|
||||
title = "Dashboard"
|
||||
titleKey = ""
|
||||
description = ""
|
||||
descriptionKey = ""
|
||||
userName = ""
|
||||
metrics = []
|
||||
actions = []
|
||||
updates = []
|
||||
tasks = []
|
||||
class = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
function chooseAction(item, index) {
|
||||
$emit("action", { item: item, value: item.value || "", index: index })
|
||||
}
|
||||
|
||||
function chooseNavigation(item, index) {
|
||||
$emit("navigate", { item: item, value: item.value || "", index: index })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<main class="wire-portal-dashboard wire-next--color-{color} wire-next--size-{size} {class}">
|
||||
<header class="wire-portal-dashboard__hero">
|
||||
<div>
|
||||
{#if eyebrowKey}<span class="wire-portal-dashboard__eyebrow" data-t="{eyebrowKey}">{eyebrow}</span>{/if}
|
||||
{#if !eyebrowKey}<span class="wire-portal-dashboard__eyebrow">{eyebrow}</span>{/if}
|
||||
<h1>
|
||||
{#if titleKey}<span data-t="{titleKey}">{title}</span>{/if}
|
||||
{#if !titleKey}<span>{title}</span>{/if}
|
||||
{#if userName}, {userName}{/if}
|
||||
</h1>
|
||||
{#if descriptionKey}<p data-t="{descriptionKey}">{description}</p>{/if}
|
||||
{#if !descriptionKey && description}<p>{description}</p>{/if}
|
||||
</div>
|
||||
<div class="wire-portal-dashboard__hero-slot"><slot name="hero-action" /></div>
|
||||
</header>
|
||||
|
||||
<section class="wire-portal-dashboard__metrics" aria-label="Summary">
|
||||
{#each metrics as item}
|
||||
<article class="wire-portal-dashboard__metric">
|
||||
<span class="wire-portal-dashboard__metric-icon {item.icon || 'icon-[lucide--activity]'}" aria-hidden="true"></span>
|
||||
<div><small>{item.label}</small><strong>{item.value}</strong>{#if item.detail}<span>{item.detail}</span>{/if}</div>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<div class="wire-portal-dashboard__grid">
|
||||
<section class="wire-portal-dashboard__panel wire-portal-dashboard__panel--actions">
|
||||
<div class="wire-portal-dashboard__panel-heading"><div><small>Shortcuts</small><h2>Quick actions</h2></div></div>
|
||||
<div class="wire-portal-dashboard__actions">
|
||||
{#each actions as item, index}
|
||||
<a href="{item.href || '#'}" @click="chooseAction(item, index)">
|
||||
<span class="{item.icon || 'icon-[lucide--arrow-up-right]'}" aria-hidden="true"></span>
|
||||
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
|
||||
<span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wire-portal-dashboard__panel">
|
||||
<div class="wire-portal-dashboard__panel-heading"><div><small>Priority</small><h2>Tasks requiring attention</h2></div></div>
|
||||
<div class="wire-portal-dashboard__list">
|
||||
{#each tasks as item, index}
|
||||
<a href="{item.href || '#'}" @click="chooseNavigation(item, index)">
|
||||
<span class="wire-portal-dashboard__status wire-portal-dashboard__status--{item.status || 'default'}"></span>
|
||||
<span><strong>{item.label}</strong>{#if item.detail}<small>{item.detail}</small>{/if}</span>
|
||||
{#if item.meta}<em>{item.meta}</em>{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wire-portal-dashboard__panel wire-portal-dashboard__panel--updates">
|
||||
<div class="wire-portal-dashboard__panel-heading"><div><small>Latest</small><h2>Recent updates</h2></div></div>
|
||||
<div class="wire-portal-dashboard__timeline">
|
||||
{#each updates as item}
|
||||
<article>
|
||||
<span class="{item.icon || 'icon-[lucide--bell]'}" aria-hidden="true"></span>
|
||||
<div><strong>{item.label}</strong>{#if item.detail}<p>{item.detail}</p>{/if}<small>{item.time || ''}</small></div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
component PreferenceSwitcher {
|
||||
props {
|
||||
@event theme = function
|
||||
@event color = function
|
||||
@event language = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
themeLabel = "Theme"
|
||||
colorLabel = "Accent color"
|
||||
languageLabel = "Language"
|
||||
languages = [
|
||||
{"label": "English", "shortLabel": "EN", "value": "en"},
|
||||
{"label": "हिन्दी", "shortLabel": "हि", "value": "hi"},
|
||||
{"label": "मराठी", "shortLabel": "म", "value": "mr"}
|
||||
]
|
||||
colors = [
|
||||
{"label": "Blue", "value": "blue", "hex": "#2563eb"},
|
||||
{"label": "Cyan", "value": "cyan", "hex": "#0891b2"},
|
||||
{"label": "Emerald", "value": "emerald", "hex": "#059669"},
|
||||
{"label": "Violet", "value": "violet", "hex": "#7c3aed"},
|
||||
{"label": "Rose", "value": "rose", "hex": "#e11d48"},
|
||||
{"label": "Amber", "value": "amber", "hex": "#d97706"}
|
||||
]
|
||||
compact = true
|
||||
class = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
function choose(type, value) {
|
||||
$emit(type, { value: value })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="wire-preferences wire-next--color-{color} wire-next--size-{size} {compact ? 'wire-preferences--compact' : ''} {class}" data-wrn-preferences aria-label="Display and language preferences">
|
||||
<details class="wire-preferences__menu" name="wire-preference-menu">
|
||||
<summary aria-label="{themeLabel}" title="{themeLabel}">
|
||||
<span class="icon-[lucide--sun-moon]" aria-hidden="true"></span>
|
||||
{#if !compact}<span>{themeLabel}</span>{/if}
|
||||
</summary>
|
||||
<div class="wire-preferences__panel">
|
||||
<strong>{themeLabel}</strong>
|
||||
<div class="wire-preferences__options">
|
||||
<button type="button" data-wire-theme-set="light" @click="choose('theme', 'light')"><span class="icon-[lucide--sun]" aria-hidden="true"></span>Light</button>
|
||||
<button type="button" data-wire-theme-set="dark" @click="choose('theme', 'dark')"><span class="icon-[lucide--moon]" aria-hidden="true"></span>Dark</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="wire-preferences__menu" name="wire-preference-menu">
|
||||
<summary aria-label="{colorLabel}" title="{colorLabel}">
|
||||
<span class="icon-[lucide--palette]" aria-hidden="true"></span>
|
||||
{#if !compact}<span>{colorLabel}</span>{/if}
|
||||
</summary>
|
||||
<div class="wire-preferences__panel wire-preferences__panel--colors">
|
||||
<strong>{colorLabel}</strong>
|
||||
<div class="wire-preferences__swatches">
|
||||
{#each colors as item}
|
||||
<button type="button" data-wire-accent-set="{item.value}" aria-label="{item.label}" title="{item.label}" style="--wire-preference-swatch: {item.hex}" @click="choose('color', item.value)"></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="wire-preferences__menu wire-preferences__menu--language" name="wire-preference-menu">
|
||||
<summary aria-label="{languageLabel}" title="{languageLabel}">
|
||||
<span class="icon-[lucide--languages]" aria-hidden="true"></span>
|
||||
<span class="wire-preferences__current-language" aria-hidden="true">EN</span>
|
||||
{#if !compact}<span>{languageLabel}</span>{/if}
|
||||
</summary>
|
||||
<div class="wire-preferences__panel wire-preferences__panel--language">
|
||||
<strong>{languageLabel}</strong>
|
||||
{#each languages as item}
|
||||
<button type="button" data-wire-lang-set="{item.value}" lang="{item.value}" @click="choose('language', item.value)">
|
||||
<span>{item.shortLabel}</span>{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,77 @@ component Sidebar {
|
||||
@event toggle = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event select = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Sidebar"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
mobileLabel = "Open navigation"
|
||||
class = ""
|
||||
}
|
||||
|
||||
state mobileOpen = false
|
||||
|
||||
functions {
|
||||
function toggleSidebar() {
|
||||
mobileOpen = !mobileOpen
|
||||
$emit("toggle", { open: mobileOpen })
|
||||
$emit(mobileOpen ? "open" : "close", { source: "mobile" })
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
mobileOpen = false
|
||||
$emit("close", { source: "mobile" })
|
||||
}
|
||||
|
||||
function selectItem(item, level) {
|
||||
mobileOpen = false
|
||||
$emit("select", { item: item, value: item.value || "", level: level })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--sidebar wire-next--{orientation} {class}" aria-label="{label}">
|
||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
||||
<slot />
|
||||
</nav>
|
||||
<div class="wire-sidebar-shell wire-next--color-{color} wire-next--size-{size} {mobileOpen ? 'is-open' : ''} {class}">
|
||||
<button type="button" class="wire-sidebar-toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleSidebar()">
|
||||
<span class="icon-[lucide--panel-left-open]" aria-hidden="true"></span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
<button type="button" class="wire-sidebar-backdrop" aria-label="Close navigation" @click="closeSidebar()"></button>
|
||||
<nav class="wire-next wire-next--sidebar wire-next--{orientation} wire-sidebar-panel" aria-label="{label}">
|
||||
<div class="wire-sidebar-panel__head">
|
||||
<strong>{label}</strong>
|
||||
<button type="button" aria-label="Close navigation" @click="closeSidebar()"><span class="icon-[lucide--x]" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="wire-sidebar-items">
|
||||
{#each items as item}
|
||||
{#if item.children && item.children.length}
|
||||
<details class="wire-sidebar-group" name="wire-sidebar-group">
|
||||
<summary>
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
<span class="icon-[lucide--chevron-down]" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="wire-sidebar-group__children">
|
||||
{#each item.children as child}
|
||||
<a href="{child.href || '#'}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
|
||||
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{child.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a href="{item.href || '#'}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<slot />
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,25 +38,136 @@ component Checkbox {
|
||||
function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
|
||||
}
|
||||
view {
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--choice-field wire-next--checkbox-field {class}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-orientation="{orientation}" data-card="{card}" data-list="{list}" data-right-aligned="{rightAligned}">
|
||||
<div class="wire-next__field-heading">{#if options.length || cornerHint}<span class="{hiddenLabel ? 'wire-next__sr-only' : ''}">{label}</span>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}{/if}</div>
|
||||
<div
|
||||
{...attrs}
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--choice-field wire-next--checkbox-field {class}"
|
||||
data-inline="{inline}"
|
||||
data-invalid="{error ? 'true' : 'false'}"
|
||||
data-orientation="{orientation}"
|
||||
data-card="{card}"
|
||||
data-list="{list}"
|
||||
data-right-aligned="{rightAligned}"
|
||||
>
|
||||
<div
|
||||
class="wire-next__field-heading"
|
||||
>
|
||||
{#if options.length || cornerHint}
|
||||
<span
|
||||
class="{hiddenLabel ? 'wire-next__sr-only' : ''}"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{#if cornerHint}
|
||||
<span
|
||||
class="wire-next__field-hint"
|
||||
>
|
||||
{cornerHint}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if options.length}
|
||||
<div class="wire-next__checkbox-group" role="group" aria-label="{hiddenLabel ? label : ''}" aria-invalid="{error ? 'true' : 'false'}">
|
||||
<div
|
||||
class="wire-next__checkbox-group"
|
||||
role="group"
|
||||
aria-label="{hiddenLabel ? label : ''}"
|
||||
aria-invalid="{error ? 'true' : 'false'}"
|
||||
>
|
||||
{#each options as option}
|
||||
<label class="wire-next__choice wire-next--checkbox" for="{id || name}-{option.value}" data-variant="{variant}" data-disabled="{disabled || option.disabled}">
|
||||
<input id="{id || name}-{option.value}" type="checkbox" name="{name}" value="{option.value}" checked="{values.includes(option.value) || option.checked}" disabled="{disabled || option.disabled}" required="{required}" readonly="{readonly}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" />
|
||||
<span class="wire-next__choice-copy"><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
|
||||
<label
|
||||
class="wire-next__choice wire-next--checkbox"
|
||||
for="{id || name}-{option.value}"
|
||||
data-variant="{variant}"
|
||||
data-disabled="{disabled || option.disabled}"
|
||||
>
|
||||
<input
|
||||
id="{id || name}-{option.value}"
|
||||
type="checkbox"
|
||||
name="{name}"
|
||||
value="{option.value}"
|
||||
checked="{values.includes(option.value) || option.checked}"
|
||||
disabled="{disabled || option.disabled}"
|
||||
required="{required}"
|
||||
readonly="{readonly}"
|
||||
@click="preventReadonly(event)"
|
||||
@input="emitField('input', event)"
|
||||
@change="emitField('change', event)"
|
||||
@focus="emitField('focus', event)"
|
||||
@blur="emitField('blur', event)"
|
||||
@invalid="emitField('invalid', event)"
|
||||
/>
|
||||
<span
|
||||
class="wire-next__choice-copy"
|
||||
>
|
||||
<strong>{option.label}</strong>
|
||||
{#if option.description}
|
||||
<small>{option.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<label class="wire-next__choice wire-next--checkbox" for="{id || name}" data-variant="{variant}" data-icon-position="{iconPosition}">
|
||||
<input id="{id || name}" type="checkbox" name="{name}" value="{value}" checked="{checked}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-checked="{indeterminate ? 'mixed' : (checked ? 'true' : 'false')}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" />
|
||||
{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}
|
||||
<span class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}">{label}</span>
|
||||
</label>
|
||||
<div
|
||||
class="wire-next__checkbox-content"
|
||||
>
|
||||
<label
|
||||
class="wire-next__choice wire-next--checkbox"
|
||||
for="{id || name}"
|
||||
data-variant="{variant}"
|
||||
data-icon-position="{iconPosition}"
|
||||
>
|
||||
<input
|
||||
id="{id || name}"
|
||||
type="checkbox"
|
||||
name="{name}"
|
||||
value="{value}"
|
||||
checked="{checked}"
|
||||
disabled="{disabled}"
|
||||
required="{required}"
|
||||
readonly="{readonly}"
|
||||
aria-checked="{indeterminate ? 'mixed' : (checked ? 'true' : 'false')}"
|
||||
aria-invalid="{error ? 'true' : 'false'}"
|
||||
@click="preventReadonly(event)"
|
||||
@input="emitField('input', event)"
|
||||
@change="emitField('change', event)"
|
||||
@focus="emitField('focus', event)"
|
||||
@blur="emitField('blur', event)"
|
||||
@invalid="emitField('invalid', event)"
|
||||
/>
|
||||
{#if icon}
|
||||
<span
|
||||
class="{icon}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span
|
||||
class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<div
|
||||
class="wire-next__checkbox-slot"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
||||
{#if helperText}
|
||||
<small
|
||||
class="wire-next__field-help"
|
||||
>
|
||||
{helperText}
|
||||
</small>
|
||||
{/if}
|
||||
<small
|
||||
class="wire-next__field-error"
|
||||
data-error="{name}"
|
||||
>
|
||||
{error}
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
+239
-1
@@ -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(85);
|
||||
expect(reference.count).toBe(90);
|
||||
expect(reference.components.map((component) => component.name)).toEqual(
|
||||
expect.arrayContaining(uiComponentNames()),
|
||||
);
|
||||
@@ -160,6 +160,234 @@ test("screenshot-directed canonical components are bundled", () => {
|
||||
expect(uiComponentNames()).toEqual(expect.arrayContaining(required));
|
||||
});
|
||||
|
||||
test("navbar supports brands, nested menus, actions, utility slots, and public events", () => {
|
||||
const source = readFileSync(uiComponentPath("Navbar"), "utf8");
|
||||
expect(source).toContain('slot name="topbar"');
|
||||
expect(source).toContain('slot name="actions"');
|
||||
expect(source).toContain("brand = {}");
|
||||
expect(source).toContain("actions = []");
|
||||
expect(source).toContain("openOnHover = false");
|
||||
expect(source).toContain('maxWidth = "full"');
|
||||
expect(source).toContain("item.children");
|
||||
expect(source).toContain("child.children");
|
||||
expect(source).toContain("wire-navbar__dropdown--{item.type || 'dropdown'}");
|
||||
for (const event of ["toggle", "open", "close", "select", "action"]) {
|
||||
expect(source).toContain(`@event ${event} = function`);
|
||||
}
|
||||
});
|
||||
|
||||
test("auth form uses package schemas, disables native validation, and renders a full-width submit control", async () => {
|
||||
const source = readFileSync(uiComponentPath("AuthForm"), "utf8");
|
||||
expect(source).toContain('return "auth-login"');
|
||||
expect(source).toContain('data-schema="{schemaName()}"');
|
||||
expect(source).toContain('data-wrnexus-runtime="auth"');
|
||||
expect(source).toContain('novalidate="true"');
|
||||
expect(source).toContain('fullWidth="true"');
|
||||
|
||||
const html = await renderComponent(source, { mode: "sign-in" });
|
||||
expect(html).toContain('data-schema="auth-login"');
|
||||
expect(html).toContain('data-wrnexus-runtime="auth"');
|
||||
expect(html).toContain("novalidate");
|
||||
expect(html).toContain("wire-auth-form__submit");
|
||||
expect(html).toContain('fullWidth="true"');
|
||||
});
|
||||
|
||||
test("footer supports typed entries, responsive columns, pre/post slots, and events", () => {
|
||||
const source = readFileSync(uiComponentPath("Footer"), "utf8");
|
||||
expect(source).toContain('slot name="pre-footer"');
|
||||
expect(source).toContain('slot name="post-footer"');
|
||||
expect(source).toContain('slot name="copyright-left"');
|
||||
expect(source).toContain('slot name="copyright-right"');
|
||||
expect(source).toContain('item.type === "header"');
|
||||
expect(source).toContain("wire-footer--columns-{columns}");
|
||||
expect(source).toContain("@event select = function");
|
||||
expect(source).toContain("@event action = function");
|
||||
});
|
||||
|
||||
test("navbar renders a brand, nested dropdowns, mega groups, and actions", async () => {
|
||||
const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
|
||||
brand: {
|
||||
label: "Police Management System",
|
||||
description: "Public portal",
|
||||
href: "/",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
label: "Services",
|
||||
type: "dropdown",
|
||||
children: [{ label: "Reports", href: "/reports" }],
|
||||
},
|
||||
{
|
||||
label: "Safety",
|
||||
type: "mega",
|
||||
columns: 2,
|
||||
children: [
|
||||
{
|
||||
label: "Resources",
|
||||
children: [{ label: "Guidance", href: "/guidance" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
actions: [{ label: "Sign in", href: "/login", variant: "primary" }],
|
||||
});
|
||||
|
||||
expect(html).toContain("Police Management System");
|
||||
expect(html).toContain("Public portal");
|
||||
expect(html).toContain("Reports");
|
||||
expect(html).toContain("Guidance");
|
||||
expect(html).toContain("wire-navbar__dropdown--mega");
|
||||
expect(html).toContain("data-wrn-navbar");
|
||||
expect(html).toContain('name="wire-navbar-menu"');
|
||||
expect(html).toContain("Sign in");
|
||||
});
|
||||
|
||||
test("navbar closes sibling menus and menus close after an outside pointer press", async () => {
|
||||
const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
|
||||
items: [
|
||||
{ label: "Services", children: [{ label: "Reports", href: "/reports" }] },
|
||||
{ label: "Safety", children: [{ label: "Guidance", href: "/guidance" }] },
|
||||
],
|
||||
});
|
||||
const dom = mountHtml(html);
|
||||
const menus = Array.from(dom.querySelectorAll(".wire-navbar__dropdown")) as HTMLDetailsElement[];
|
||||
|
||||
menus[0].open = true;
|
||||
menus[0].dispatchEvent(
|
||||
new (dom.window as { Event: typeof Event }).Event("toggle", { bubbles: false }),
|
||||
);
|
||||
menus[1].open = true;
|
||||
menus[1].dispatchEvent(
|
||||
new (dom.window as { Event: typeof Event }).Event("toggle", { bubbles: false }),
|
||||
);
|
||||
expect(menus[0].open).toBe(false);
|
||||
expect(menus[1].open).toBe(true);
|
||||
|
||||
dom.document.body.dispatchEvent(
|
||||
new (dom.window as { Event: typeof Event }).Event("pointerdown", { bubbles: true }),
|
||||
);
|
||||
expect(menus[1].open).toBe(false);
|
||||
});
|
||||
|
||||
test("navbar optionally opens menus on hover and renders full-width mode", async () => {
|
||||
const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
|
||||
openOnHover: true,
|
||||
maxWidth: "full",
|
||||
items: [{ label: "Services", children: [{ label: "Reports", href: "/reports" }] }],
|
||||
});
|
||||
const dom = mountHtml(html);
|
||||
const navbar = dom.querySelector("[data-wrn-navbar]");
|
||||
const menu = dom.querySelector(".wire-navbar__dropdown") as HTMLDetailsElement;
|
||||
|
||||
expect(navbar?.classList.contains("wire-navbar--width-full")).toBe(true);
|
||||
expect(navbar?.getAttribute("data-open-on-hover")).toBe("true");
|
||||
menu.dispatchEvent(new (dom.window as { Event: typeof Event }).Event("pointerenter"));
|
||||
expect(menu.open).toBe(true);
|
||||
menu.dispatchEvent(new (dom.window as { Event: typeof Event }).Event("pointerleave"));
|
||||
expect(menu.open).toBe(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 280));
|
||||
expect(menu.open).toBe(false);
|
||||
});
|
||||
|
||||
test("navbar hover menu stays open while the pointer crosses into its panel", async () => {
|
||||
const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
|
||||
openOnHover: true,
|
||||
items: [{ label: "Services", children: [{ label: "Reports", href: "/reports" }] }],
|
||||
});
|
||||
const dom = mountHtml(html);
|
||||
const menu = dom.querySelector(".wire-navbar__dropdown") as HTMLDetailsElement;
|
||||
const panel = dom.querySelector(".wire-navbar__panel") as HTMLElement;
|
||||
const DomEvent = (dom.window as { Event: typeof Event }).Event;
|
||||
|
||||
menu.dispatchEvent(new DomEvent("pointerenter"));
|
||||
menu.dispatchEvent(new DomEvent("pointerleave"));
|
||||
panel.dispatchEvent(new DomEvent("pointerenter", { bubbles: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 280));
|
||||
|
||||
expect(menu.open).toBe(true);
|
||||
});
|
||||
|
||||
test("preference switcher keeps only one popover open and closes outside", async () => {
|
||||
const html = await renderComponent(
|
||||
readFileSync(uiComponentPath("PreferenceSwitcher"), "utf8"),
|
||||
{},
|
||||
);
|
||||
const dom = mountHtml(html);
|
||||
const menus = Array.from(dom.querySelectorAll(".wire-preferences__menu")) as HTMLDetailsElement[];
|
||||
const DomEvent = (dom.window as { Event: typeof Event }).Event;
|
||||
|
||||
menus[0].open = true;
|
||||
menus[0].dispatchEvent(new DomEvent("toggle"));
|
||||
menus[1].open = true;
|
||||
menus[1].dispatchEvent(new DomEvent("toggle"));
|
||||
expect(menus[0].open).toBe(false);
|
||||
expect(menus[1].open).toBe(true);
|
||||
|
||||
dom.document.body.dispatchEvent(new DomEvent("pointerdown", { bubbles: true }));
|
||||
expect(menus[1].open).toBe(false);
|
||||
});
|
||||
|
||||
test("sidebar renders compact grouped submenu navigation and a mobile drawer trigger", async () => {
|
||||
const html = await renderComponent(readFileSync(uiComponentPath("Sidebar"), "utf8"), {
|
||||
label: "Operations",
|
||||
orientation: "vertical",
|
||||
items: [
|
||||
{ label: "Dashboard", href: "/dashboard", value: "dashboard" },
|
||||
{
|
||||
label: "Case management",
|
||||
children: [
|
||||
{ label: "Assigned cases", href: "/cases", value: "cases" },
|
||||
{ label: "Evidence", href: "/evidence", value: "evidence" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(html).toContain("wire-sidebar-toggle");
|
||||
expect(html).toContain("wire-sidebar-backdrop");
|
||||
expect(html).toContain("wire-sidebar-group");
|
||||
expect(html).toContain("Assigned cases");
|
||||
expect(html).toContain("Evidence");
|
||||
});
|
||||
|
||||
test("dropdown renders slotted account triggers, menu structure, and public events", async () => {
|
||||
const source = readFileSync(uiComponentPath("Dropdown"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
label: "Account menu",
|
||||
items: [
|
||||
{ type: "header", label: "Account" },
|
||||
{ label: "Profile", href: "/profile", icon: "icon-[lucide--user]" },
|
||||
{ type: "divider" },
|
||||
{ label: "Sign out", href: "/logout", danger: true },
|
||||
],
|
||||
});
|
||||
|
||||
expect(source).toContain('slot name="trigger"');
|
||||
expect(source).toContain("@event select = function");
|
||||
expect(html).toContain("wire-dropdown__panel");
|
||||
expect(html).toContain("Profile");
|
||||
expect(html).toContain("wire-dropdown__item--danger");
|
||||
});
|
||||
|
||||
test("footer renders header and link entries with the requested column count", async () => {
|
||||
const html = await renderComponent(readFileSync(uiComponentPath("Footer"), "utf8"), {
|
||||
columns: 4,
|
||||
items: [
|
||||
{ type: "header", label: "Support" },
|
||||
{ type: "link", label: "Contact", href: "/contact" },
|
||||
],
|
||||
copyright: "Public service platform",
|
||||
});
|
||||
|
||||
expect(html).toContain("wire-footer--columns-4");
|
||||
expect(html).toContain("wire-footer__column");
|
||||
expect(html).toContain("wire-footer__column-links");
|
||||
expect(html).toContain("wire-footer__heading");
|
||||
expect(html).toContain("Support");
|
||||
expect(html).toContain('href="/contact"');
|
||||
expect(html).toContain("Public service platform");
|
||||
});
|
||||
|
||||
test("component-system CSS includes responsive, theme-token, focus, and reduced-motion rules", () => {
|
||||
const css = uiCss();
|
||||
expect(css).toContain("@media (max-width: 768px)");
|
||||
@@ -184,6 +412,16 @@ test("component-system CSS includes responsive, theme-token, focus, and reduced-
|
||||
expect(css).toContain(".wire-next--table");
|
||||
expect(css).toContain('[data-show="false"]');
|
||||
expect(css).toContain("display: none !important");
|
||||
expect(css).toMatch(
|
||||
/\.wire-navbar__brand-copy strong\s*\{[^}]*font-size: 0\.9rem;[^}]*font-weight: 600;/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/\.wire-navbar__menu-link,[\s\S]*?font-size: 0\.8125rem;[\s\S]*?font-weight: 500;/,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/\.wire-footer__heading\s*\{[^}]*font-size: 0\.8125rem;[^}]*font-weight: 600;/s,
|
||||
);
|
||||
expect(css).toMatch(/\.wire-footer__link\s*\{[^}]*font-size: 0\.8125rem;[^}]*font-weight: 400;/s);
|
||||
});
|
||||
|
||||
test("component boundaries have no default margins and expose the canonical class prop", () => {
|
||||
|
||||
+1552
-14
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user