Compare commits

..
85 Commits
Author SHA1 Message Date
Clintchiz 87507edf59 release: WRNexusJS 0.7.0 2026-08-01 10:04:42 +05:30
Clintchiz c54144f2e4 fix(release): format generated UI references before verification 2026-08-01 07:14:45 +05:30
Clintchiz 2b06b00d35 fix(release): format generated UI references before verification 2026-08-01 07:14:16 +05:30
Clintchiz 28d931dfff docs(ui): refresh component reference 2026-08-01 07:11:54 +05:30
Clintchiz e2f7bea299 fix(release): stabilize generated UI references 2026-08-01 07:02:22 +05:30
Clintchiz d5e834bc78 release: WRNexusJS 0.6.0 2026-08-01 06:50:37 +05:30
Clintchiz 687d345882 release: WRNexusJS 0.6.0 2026-08-01 01:09:58 +05:30
Clintchiz 3e565e8d03 Pre Release New Changes 2026-07-31 16:30:13 +05:30
Clintchiz 358a520bc5 release: WRNexusJS 0.5.14 2026-07-30 21:29:25 +05:30
Clintchiz 9dffe83f32 release: WRNexusJS 0.5.13 2026-07-30 20:46:47 +05:30
Clintchiz d64e899993 release: WRNexusJS 0.5.12 2026-07-30 18:59:01 +05:30
Clintchiz f37303b22d release: WRNexusJS 0.5.11 2026-07-30 15:03:52 +05:30
Clintchiz d1b0c55b53 release: WRNexusJS 0.5.10 2026-07-30 13:36:29 +05:30
Clintchiz 8fc6f15402 feat(wrn): support native structured prop expressions 2026-07-29 16:46:18 +05:30
Clintchiz 6afe32f63f release: WRNexusJS 0.5.0 2026-07-29 12:51:10 +05:30
Clintchiz 76c768099d release: WRNexusJS 0.4.0 2026-07-27 12:54:43 +05:30
Clintchiz 30e5721e84 release: WRNexusJS 0.4.0 2026-07-27 12:42:18 +05:30
Clintchiz 8b728a3e5d New Captcha Package added 2026-07-25 13:38:18 +05:30
Clintchiz d0aded0392 release: WRNexusJS 0.3.6 2026-07-24 15:30:48 +05:30
Clintchiz c81dedff17 release: WRNexusJS 0.3.5 2026-07-24 12:46:44 +05:30
Clintchiz 44ba847210 release: WRNexusJS 0.3.4 2026-07-22 21:48:39 +05:30
Clintchiz 798f06d4c0 release: WRNexusJS 0.3.4 2026-07-22 21:44:57 +05:30
Clintchiz 173e230d7f release: WRNexusJS 0.3.4 2026-07-22 21:40:38 +05:30
Clintchiz bbec4cc7bb docs(ui): refresh component reference 2026-07-22 21:38:20 +05:30
Clintchiz 504bd4358f release: WRNexusJS 0.3.4 2026-07-22 21:37:23 +05:30
Clintchiz e543fd026d release: WRNexusJS 0.3.4 2026-07-22 21:30:37 +05:30
Clintchiz d51662826d release: WRNexusJS 0.3.4 2026-07-22 21:25:53 +05:30
Clintchiz e2d9ea1ece release: WRNexusJS 0.3.3 2026-07-22 20:37:59 +05:30
Clintchiz 58ba2f2046 release: WRNexusJS 0.3.2 2026-07-22 20:10:44 +05:30
Clintchiz 304819dfe9 release: WRNexusJS 0.3.1 2026-07-22 18:42:20 +05:30
Clintchiz 07d8fb59d6 release: WRNexusJS 0.3.0 2026-07-22 17:29:08 +05:30
Clintchiz 13dfa31d19 release: WRNexusJS 0.2.79 2026-07-22 12:56:07 +05:30
Clintchiz c6fc0f1f63 release: WRNexusJS 0.2.78 2026-07-22 01:53:18 +05:30
Clintchiz 7ff5b3e8c5 release: WRNexusJS 0.2.77 2026-07-22 01:26:10 +05:30
Clintchiz 569365143b release: WRNexusJS 0.2.76 2026-07-21 13:09:09 +05:30
Clintchiz 69b6cd431f release: WRNexusJS 0.2.75 2026-07-21 12:30:46 +05:30
Clintchiz 2ca2d02b22 feat: support WRN imports and dynamic public shell 2026-07-21 11:15:06 +05:30
Clintchiz 0013c0771d chore(release): prepare WRNexusJS 0.2.73 2026-07-20 16:34:20 +05:30
Clintchiz 0b8856b3b5 chore(release): prepare WRNexusJS 0.2.72 2026-07-20 16:15:03 +05:30
Clintchiz 45e6fd3cb9 fix(dev-server): invalidate WRN cache by content 2026-07-20 16:07:38 +05:30
Clintchiz 54f5309fca fix(ui): render polished SSR-safe component variants 2026-07-20 15:55:38 +05:30
Clintchiz 944f83d3f4 fix: use public origins for SSO redirects 2026-07-20 15:04:20 +05:30
Clintchiz a75779fa4d fix: pin production runtime in applications 2026-07-20 14:45:56 +05:30
Clintchiz 2b4083c6db fix: surface app errors in gateway logs 2026-07-20 14:26:27 +05:30
Clintchiz aa2595ec01 fix: log production request failures 2026-07-20 13:48:47 +05:30
Clintchiz b3e5e5b999 feat: configure workspace environment runtimes 2026-07-20 09:22:26 +05:30
Clintchiz 3888453e0d build: stage private packages for 0.2.66 2026-07-20 00:10:16 +05:30
Clintchiz b95bdc6e77 release: prepare WRNexusJS 0.2.66 2026-07-20 00:10:04 +05:30
Clintchiz 1745d8d676 feat: add reusable public chrome and named environments 2026-07-20 00:03:35 +05:30
Clintchiz f72aec7c8c feat(ui): add professional visual fallbacks 2026-07-19 23:09:56 +05:30
Clintchiz 33730c68ab fix(gateway): keep production app ports private 2026-07-19 22:24:28 +05:30
Clintchiz 5ce45b4973 feat(cli): add production workspace command 2026-07-19 21:57:55 +05:30
Clintchiz dcdb5e766d fix(ui): polish linked catalog cards 2026-07-19 21:29:25 +05:30
Clintchiz a3e21163c1 feat(ui): expose app theme contract and public components 2026-07-19 20:23:20 +05:30
Clintchiz c5326f7622 fix: normalize component boundary spacing 2026-07-19 20:08:14 +05:30
Clintchiz eaea2c5f72 fix: keep page shells transparent 2026-07-19 19:52:14 +05:30
Clintchiz ca8f746f41 chore: ignore packaged VS Code extensions 2026-07-19 18:58:31 +05:30
Clintchiz 94ae40d8bd feat: add typed WRN declarations 2026-07-19 18:44:27 +05:30
Clintchiz 0d3ec79ee4 release: complete UI component catalog 0.2.58 2026-07-19 18:14:06 +05:30
Clintchiz c0c09426ff release: WRNexusJS 0.2.57 2026-07-19 17:45:13 +05:30
Clintchiz 14e2878417 fix: tolerate autocrlf publish staging 2026-07-19 17:29:03 +05:30
Clintchiz 180239ef4c release: WRNexusJS 0.2.56 2026-07-19 17:25:37 +05:30
Clintchiz 12beb6f4ba release: WRNexusJS 0.2.55 2026-07-19 17:14:12 +05:30
Clintchiz 6dbc75c370 release: WRNexusJS 0.2.54 2026-07-19 15:51:27 +05:30
Clintchiz 7cee13526e release: WRNexusJS 0.2.53 2026-07-19 14:40:06 +05:30
Clintchiz 3bb096057e release: WRNexusJS 0.2.52 2026-07-19 14:23:10 +05:30
Clintchiz 646d16f83d fix(vscode): repair WRN language diagnostics and activation 2026-07-19 14:02:17 +05:30
Clintchiz df4c1d3e7d release: WRNexusJS 0.2.51 2026-07-19 13:38:23 +05:30
Clintchiz 17dc7c1a06 release: WRNexusJS 0.2.50 2026-07-19 13:08:49 +05:30
Clintchiz c72cbbd780 release: WRNexusJS 0.2.49 2026-07-19 12:29:13 +05:30
Clintchiz 1cda2a01b3 release: WRNexusJS 0.2.49 2026-07-19 12:03:49 +05:30
Clintchiz 9ca1cc6b0f release: WRNexusJS 0.2.49 2026-07-18 19:33:57 +05:30
Clintchiz aa6badbd31 release: WRNexusJS 0.2.48 2026-07-18 19:11:00 +05:30
Clintchiz 4a3ee3baa7 release: WRNexusJS 0.2.48 2026-07-18 19:07:23 +05:30
Clintchiz 95ca1c0625 New UI Components 2026-07-18 16:00:13 +05:30
Clintchiz d22824fa62 release: WRNexusJS 0.2.47 2026-07-18 13:12:16 +05:30
Clintchiz 973d580bc5 release: WRNexusJS 0.2.46 2026-07-15 20:09:47 +05:30
Clintchiz 8a8328a308 release: WRNexusJS 0.2.44 2026-07-15 19:41:42 +05:30
Clintchiz 0f915c3c98 release: WRNexusJS 0.2.44 2026-07-15 18:54:29 +05:30
Clintchiz 9aa13fb4db release: WRNexusJS 0.2.43 2026-07-15 18:41:55 +05:30
Clintchiz 9070a1ef9c release: WRNexusJS 0.2.42 2026-07-15 18:15:22 +05:30
Clintchiz 4230de7065 release: WRNexusJS 0.2.41 2026-07-15 14:52:56 +05:30
Clintchiz 0006aef2c4 release: WRNexusJS 0.2.40 2026-07-15 14:02:27 +05:30
Clintchiz 7dfca4a190 release: WRNexusJS 0.2.40 2026-07-15 13:42:18 +05:30
Clintchiz 29bbeefe92 release: WRNexusJS 0.2.39 2026-07-15 13:22:20 +05:30
1131 changed files with 221288 additions and 6015 deletions
View File
+12
View File
@@ -7,3 +7,15 @@ dist/
*.db-wal *.db-wal
.DS_Store .DS_Store
bun.lockb bun.lockb
.publish/
# Environment files may contain secrets. Commit only templates.
.env
.env.*
!.env.example
!.env.*.example
# Local focused typecheck helpers must never enter the repository.
focus-shims.d.ts
tsconfig.focus.json
+6
View File
@@ -15,3 +15,9 @@ bun.lockb
editors/vscode/src/compiler.cjs editors/vscode/src/compiler.cjs
*.svg *.svg
**/.vscodeignore **/.vscodeignore
# Local focused typecheck helpers (never part of a release)
focus-shims.d.ts
**/focus-shims.d.ts
tsconfig.focus.json
**/tsconfig.focus.json
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+12
View File
@@ -36,6 +36,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). |
| `wrnexus workspace add <name>` | Add and register an app in the current workspace. | | `wrnexus workspace add <name>` | Add and register an app in the current workspace. |
| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. | | `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. |
| `wrnexus production [workspace-dir]` | Build, migrate, and serve every workspace app in production. |
| `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. |
| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). |
| `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. | | `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. |
@@ -168,6 +169,17 @@ wrnexus workspace acme
wrnexus gateway --port=3000 wrnexus gateway --port=3000
``` ```
For a complete production start, use the first-class workspace orchestrator:
```bash
wrnexus production --host=0.0.0.0 --port=3000
```
It builds every registered app, applies default and named-database SQL migrations
when present, and starts the production gateway only after preparation succeeds.
Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are
managed separately; `--environment=<name>` selects another workspace environment.
From a workspace root, add and register another app in one command: From a workspace root, add and register another app in one command:
```bash ```bash
+14 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.", "description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -28,16 +28,18 @@
"wrnexus": "./dist/index.js" "wrnexus": "./dist/index.js"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.38", "@wrnexus/core": "^0.7.0",
"@wrnexus/router": "^0.2.38", "@wrnexus/router": "^0.7.0",
"@wrnexus/csr": "^0.2.38", "@wrnexus/csr": "^0.7.0",
"@wrnexus/compiler": "^0.2.38", "@wrnexus/compiler": "^0.7.0",
"@wrnexus/styles": "^0.2.38", "@wrnexus/styles": "^0.7.0",
"@wrnexus/dev-server": "^0.2.38", "@wrnexus/dev-server": "^0.7.0",
"@wrnexus/ui": "^0.2.38", "@wrnexus/ui": "^0.7.0",
"@wrnexus/validation": "^0.2.38", "@wrnexus/validation": "^0.7.0",
"@wrnexus/i18n": "^0.2.38", "@wrnexus/i18n": "^0.7.0",
"@wrnexus/db": "^0.2.38" "@wrnexus/db": "^0.7.0",
"@wrnexus/plugin": "^0.7.0",
"@wrnexus/syntax": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+32 -16
View File
@@ -8,6 +8,19 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page. `@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
Static ES module imports may appear before the root declaration. Imported values are
available to server-rendered expressions, including component props:
```wrn
import { appUrl } from "@wrnexus/helpers";
layout PublicLayout {
view {
<PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
}
}
```
## Installation ## Installation
```bash ```bash
@@ -76,18 +89,19 @@ class Lexer {
Exported type-only symbols describing the parsed tree: Exported type-only symbols describing the parsed tree:
| Type | Description | | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst` | Root node: `kind` (`"page" \| "component"`), `name`, optional `layout`, `props`, `states`, `seo`, `view`, `styles`, `functions`, `dataApis`, `modeFunctions`, `apis`, `realtimes`. | | `PageAst` | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. | | `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. |
| `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. | | `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. |
| `StateDecl` | `{ name; expr }` — a `state x = <expr>` declaration. | | `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. |
| `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. | | `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. |
| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. | | `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. |
| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. | | `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. |
| `DataMode` | `"ssr" \| "client"`. | | `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. | | `DataMode` | `"ssr" \| "client"`. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. | | `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. |
## Usage ## Usage
@@ -147,12 +161,14 @@ lx.next(); // { type: "lbrace", value: "{", pos: 10 }
A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` body containing zero or more members: A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` body containing zero or more members:
- `layout = "<name>"` — selects `app/layouts/<name>.wrn` (pages only). - `layout = "<name>"` — selects `app/layouts/<name>.wrn` (pages only).
- `props { name = <default> ... }` — component props; each default's type drives coercion. - `types { <TypeScript declarations> }` — reusable interfaces and aliases for the current file.
- `state <ident> = <expr>` — reactive state seeded from a raw JS expression. - `props { name: Type = <default> ... }` — typed component props. Omit `= <default>` to make a prop required. Legacy inferred props remain supported.
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Attribute expressions that reference `state` keep an SSR value and update reactively in the browser. - `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`.
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility.
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser.
- `seo { key = "value" ... }` — metadata merged into the generated `meta`. - `seo { key = "value" ... }` — metadata merged into the generated `meta`.
- `style { <raw css> }` — inlined page/component stylesheet (repeatable). - `style { <raw css> }` — inlined page/component stylesheet (repeatable).
- `functions { <raw js> }` — shared server-side helpers (repeatable). - `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code.
- `api <METHOD> <path> { <raw js> }` — route handler, lowered to a `METHOD` export (repeatable). - `api <METHOD> <path> { <raw js> }` — route handler, lowered to a `METHOD` export (repeatable).
- `ssr { ... }` / `client { ... }` — data blocks holding `api <name> <METHOD> <path> { ... }` bindings and their own `functions { ... }`. - `ssr { ... }` / `client { ... }` — data blocks holding `api <name> <METHOD> <path> { ... }` bindings and their own `functions { ... }`.
- `realtime <name> { on <evt>(<args>) { <raw js> } ... }` — websocket handlers, lowered to a `websocket` export. - `realtime <name> { on <evt>(<args>) { <raw js> } ... }` — websocket handlers, lowered to a `websocket` export.
+6 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.", "description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -20,6 +20,10 @@
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"dependencies": {
"@wrnexus/syntax": "^0.7.0",
"@wrnexus/store": "^0.7.0"
},
"files": [ "files": [
"dist" "dist"
] ]
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+10 -9
View File
@@ -49,15 +49,16 @@ getRealtimeRuntime(): string // → REALTIME_RUNTIME
Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works. Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works.
| Directive | Purpose | | Directive | Purpose |
| -------------------------------------------------- | ----------------------------------------------------------------------- | | -------------------------------------------------------- | ----------------------------------------------------------------------- |
| `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree | | `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree |
| `data-on-<event>="count++"` | Run a statement in scope on a DOM event | | `data-on-<event>="count++"` | Run a statement in scope on a DOM event |
| `data-text="expr"` | Bind an element's `textContent` to an expression | | `data-text="expr"` | Bind an element's `textContent` to an expression |
| `data-show="expr"` | Toggle visibility (`display`) on truthiness | | `data-show="expr"` | Toggle visibility (`display`) on truthiness |
| `data-for="item in list"` (opt. `item, i in list`) | Per-item list rendering template | | `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder |
| `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values | | `data-key="item.id"` | Alternative key declaration for `data-for` templates |
| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) | | `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values |
| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) |
Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it. Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it.
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.", "description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.38" "@wrnexus/core": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.", "description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+19 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.", "description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -25,18 +25,23 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.38", "@wrnexus/core": "^0.7.0",
"@wrnexus/router": "^0.2.38", "@wrnexus/dev-toolbar": "^0.7.0",
"@wrnexus/ssr": "^0.2.38", "@wrnexus/router": "^0.7.0",
"@wrnexus/csr": "^0.2.38", "@wrnexus/ssr": "^0.7.0",
"@wrnexus/compiler": "^0.2.38", "@wrnexus/csr": "^0.7.0",
"@wrnexus/styles": "^0.2.38", "@wrnexus/compiler": "^0.7.0",
"@wrnexus/ui": "^0.2.38", "@wrnexus/styles": "^0.7.0",
"@wrnexus/validation": "^0.2.38", "@wrnexus/ui": "^0.7.0",
"@wrnexus/i18n": "^0.2.38", "@wrnexus/validation": "^0.7.0",
"@wrnexus/db": "^0.2.38", "@wrnexus/i18n": "^0.7.0",
"@wrnexus/pubsub": "^0.2.38", "@wrnexus/db": "^0.7.0",
"@wrnexus/uploader": "^0.2.38" "@wrnexus/pubsub": "^0.7.0",
"@wrnexus/uploader": "^0.7.0",
"@wrnexus/plugin": "^0.7.0",
"@wrnexus/store": "^0.7.0",
"@wrnexus/security": "^0.7.0",
"@wrnexus/observability": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+23
View File
@@ -0,0 +1,23 @@
# @wrnexus/dev-toolbar
Development-only page quality toolbar for WRNexusJS.
## Features
- Runtime, resource and unhandled promise error capture
- Accessibility, SEO, image, media, color, HTML, form, link, responsive and security checks
- Performance and network observations
- Element highlighting and issue filtering
- Server-side issue collector
- Development-only asset strings for direct serving by `@wrnexus/dev-server`
- Safe open-in-editor helper
## Dev-server integration
Serve `DEV_TOOLBAR_RUNTIME` at `/__wrnexus/dev-toolbar.js` and `DEV_TOOLBAR_CSS` at `/__wrnexus/dev-toolbar.css`, then inject this before `</body>` in development:
```html
<script type="module" src="/__wrnexus/dev-toolbar.js" data-wrnexus-dev-toolbar></script>
```
The browser runtime exposes `window.__wrnexusDevToolbar`.
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.7.0",
"type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"import": "./dist/server/index.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./rules": {
"types": "./dist/rules/index.d.ts",
"import": "./dist/rules/index.js"
}
},
"files": [
"dist"
]
}
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.", "description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.38" "@wrnexus/core": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.", "description": "@wrnexus/i18n — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.38" "@wrnexus/core": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.", "description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.", "description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/native": "^0.2.38" "@wrnexus/native": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.", "description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.", "description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+7
View File
@@ -0,0 +1,7 @@
# @wrnexus/plugin
Deterministic WRNexusJS plugin contracts for configuration, AST/code transforms,
diagnostics, development servers, production builds, and DevToolbar extensions.
Use `definePlugin()` and declare `enforce`, `before`, or `after` when ordering matters.
Duplicate names and dependency cycles are rejected.
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@wrnexus/plugin",
"version": "0.7.0",
"type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./manifest": {
"types": "./dist/manifest.d.ts",
"import": "./dist/manifest.js"
},
"./discovery": {
"types": "./dist/discovery.d.ts",
"import": "./dist/discovery.js"
}
},
"dependencies": {
"@wrnexus/syntax": "^0.7.0"
},
"files": [
"dist"
]
}
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.", "description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.", "description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.", "description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -21,8 +21,8 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/compiler": "^0.2.38", "@wrnexus/compiler": "^0.7.0",
"@wrnexus/core": "^0.2.38" "@wrnexus/core": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+13 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.", "description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -18,10 +18,20 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
},
"./rpc": {
"types": "./dist/rpc.d.ts",
"import": "./dist/rpc.js"
},
"./store-context": {
"types": "./dist/store-context.d.ts",
"import": "./dist/store-context.js"
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.38" "@wrnexus/core": "^0.7.0",
"@wrnexus/store": "^0.7.0",
"@wrnexus/security": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+26 -1
View File
@@ -106,6 +106,7 @@ Tokens are emitted as `--wire-<key>` custom properties, **except** the reserved
type ThemeTokens = Record<string, string>; type ThemeTokens = Record<string, string>;
interface ThemeConfig { interface ThemeConfig {
palette?: ThemePaletteName | CustomThemePalette;
default?: string; // theme used when no cookie is present default?: string; // theme used when no cookie is present
themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
} }
@@ -117,7 +118,30 @@ interface ResolvedTheme {
} }
``` ```
Built-in token keys (both `light` and `dark`): `color-scheme`, `color-bg`, `color-surface`, `color-surface-2`, `color-text`, `color-muted`, `color-border`, `color-primary`, `color-primary-hover`, `color-primary-contrast`, `color-danger`, `color-success`, `color-warning`, `radius`, `radius-sm`, `font-sans`, `shadow-1`. Built-in palettes are `blue`, `indigo`, `violet`, `emerald`, `cyan`, `rose`,
`amber`, and `slate`. Each supplies primary, secondary, info, success, warning,
danger/error, hover, and contrast colors to every light/dark theme. Built-in
token keys also include surfaces, text, borders, radii, fonts, and shadows.
A custom palette is intentionally complete, so components never fall back to an
unrelated blue status or action color:
```ts
theme: {
palette: {
primary: "#7c3aed",
primaryHover: "#6d28d9",
primaryContrast: "#ffffff",
secondary: "#db2777",
secondaryHover: "#be185d",
secondaryContrast: "#ffffff",
info: "#2563eb",
success: "#059669",
warning: "#d97706",
danger: "#dc2626",
},
}
```
The client runtime (`renderThemeRuntime`) exposes `window.wireTheme` with `{ get, set, toggle, bind, themes }`, wires up any `[data-wire-theme-toggle]` and `[data-wire-theme-set]` elements on load, and persists the choice to the `wire-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order. The client runtime (`renderThemeRuntime`) exposes `window.wireTheme` with `{ get, set, toggle, bind, themes }`, wires up any `[data-wire-theme-toggle]` and `[data-wire-theme-set]` elements on load, and persists the choice to the `wire-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order.
@@ -135,6 +159,7 @@ export default {
port: 3000, port: 3000,
db: { driver: "sqlite", url: "app.db" }, db: { driver: "sqlite", url: "app.db" },
theme: { theme: {
palette: "violet",
default: "dark", default: "dark",
themes: { themes: {
light: { "color-primary": "#7c3aed" }, // override one token; rest inherited light: { "color-primary": "#7c3aed" }, // override one token; rest inherited
+5 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.", "description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
@@ -21,7 +21,9 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/uploader": "^0.2.38" "@wrnexus/uploader": "^0.7.0",
"@wrnexus/core": "^0.7.0",
"@wrnexus/plugin": "^0.7.0"
}, },
"files": [ "files": [
"dist" "dist"
+7
View File
@@ -0,0 +1,7 @@
# @wrnexus/syntax
Canonical WRN lexer, parser, AST, language metadata, source positions, and stable
diagnostics. Framework tooling should import this package instead of implementing a
separate `.wrn` parser.
See `docs/WRN-LANGUAGE-SPEC-1.0.md` in the WRNexusJS repository.
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@wrnexus/syntax",
"version": "0.7.0",
"type": "module",
"description": "@wrnexus/syntax — part of the WrNexus framework.",
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./parser": {
"types": "./dist/parser.d.ts",
"import": "./dist/parser.js"
},
"./tokenizer": {
"types": "./dist/tokenizer.d.ts",
"import": "./dist/tokenizer.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./diagnostics": {
"types": "./dist/diagnostics.d.ts",
"import": "./dist/diagnostics.js"
},
"./spec": {
"types": "./dist/spec.d.ts",
"import": "./dist/spec.js"
}
},
"files": [
"dist"
]
}
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.", "description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.38", "version": "0.7.0",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -8,7 +8,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"engines": { "engines": {
"bun": ">=1.1.0" "bun": ">=1.3.0"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
+997
View File
@@ -0,0 +1,997 @@
# WRNexus UI component reference
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
### AdvancedSelect
Theme-aware, responsive advanced select component.
- Mount: `data-component="AdvancedSelect"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Advanced Select"`, `name: string = ""`, `value: string = ""`, `values: unknown[] = []`, `options: unknown[] = []`, `groups: unknown[] = []`, `placeholder: string = "Select an option"`, `placeholderIcon: string = ""`, `searchPlaceholder: string = "Search options…"`, `multiple: boolean = false`, `searchable: boolean = true`, `defaultOpen: boolean = false`, `clearable: boolean = true`, `allowEmpty: boolean = true`, `tags: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `helpText: string = ""`, `loading: boolean = false`, `loadingLabel: string = "Loading options…"`, `emptyLabel: string = "No options found"`, `selectedOptionsLabel: string = "Selected options"`, `clearLabel: string = "Clear selection"`, `createLabel: string = "Create"`, `loadMoreLabel: string = "Load more"`, `searchMode: string = "contains"`, `searchFields: string = "label,description"`, `minSearchLength: number = 0`, `searchResultLimit: number = 0`, `maxSelections: number = 0`, `showCounter: boolean = false`, `counterTemplate: string = "{selected} selected"`, `optionTemplate: string = "default"`, `selectedTemplate: string = "default"`, `closeOnSelect: boolean = true`, `scrollToSelected: boolean = true`, `fixed: boolean = false`, `placement: string = "bottom"`, `remote: boolean = false`, `remoteUrl: string = ""`, `remoteQueryParam: string = "q"`, `remoteDebounce: number = 250`, `remoteAutoLoad: boolean = true`, `infinite: boolean = false`, `hasMore: boolean = false`, `page: number = 1`, `class: string = ""`
- Slots: None
- Outputs: `search({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### ComboBox
Editable autocomplete combobox with local and remote suggestions.
- Mount: `data-component="ComboBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "ComboBox"`, `name: string = ""`, `value: string = ""`, `options: unknown[] = []`, `groups: unknown[] = []`, `placeholder: string = "Search or select an option"`, `searchPlaceholder: string = "Start typing…"`, `clearable: boolean = true`, `allowCustomValue: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `helpText: string = ""`, `loading: boolean = false`, `loadingLabel: string = "Loading suggestions…"`, `emptyLabel: string = "No matching options"`, `clearLabel: string = "Clear value"`, `toggleLabel: string = "Toggle suggestions"`, `searchMode: string = "contains"`, `searchFields: string = "label,description"`, `minSearchLength: number = 0`, `searchResultLimit: number = 0`, `optionTemplate: string = "default"`, `defaultOpen: boolean = false`, `closeOnSelect: boolean = true`, `fixed: boolean = false`, `placement: string = "bottom"`, `autocomplete: string = "off"`, `remote: boolean = false`, `remoteUrl: string = ""`, `remoteQueryParam: string = "q"`, `remoteDebounce: number = 250`, `remoteAutoLoad: boolean = true`, `infinite: boolean = false`, `hasMore: boolean = false`, `page: number = 1`, `loadMoreLabel: string = "Load more"`, `class: string = ""`
- Slots: None
- Outputs: `search({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### CopyMarkup
Theme-aware, responsive copy markup component.
- Mount: `data-component="CopyMarkup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Copy Markup"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "text"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `copy({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### InputNumber
Theme-aware, responsive input number component.
- Mount: `data-component="InputNumber"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`, `id: string = ""`, `name: string = "quantity"`, `value: number = 0`, `min: string = ""`, `max: string = ""`, `step: number = 1`, `precision: string = "auto"`, `label: string = ""`, `description: string = ""`, `helpText: string = ""`, `error: string = ""`, `invalid: boolean = false`, `prefix: string = ""`, `suffix: string = ""`, `placeholder: string = ""`, `autocomplete: string = "off"`, `inputMode: string = "decimal"`, `ariaLabel: string = ""`, `required: boolean = false`, `disabled: boolean = false`, `inputDisabled: boolean = false`, `buttonsDisabled: boolean = false`, `readonly: boolean = false`, `allowInput: boolean = true`, `keyboard: boolean = true`, `wheel: boolean = false`, `clamp: boolean = true`, `fullWidth: boolean = false`, `showButtons: boolean = true`, `showValidationMessage: boolean = true`, `decrementLabel: string = "Decrease value"`, `incrementLabel: string = "Increase value"`, `controlsLabel: string = "Quantity controls"`, `requiredMessage: string = "A value is required."`, `minMessage: string = "Value is below the minimum."`, `maxMessage: string = "Value is above the maximum."`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `increment({ value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `decrement({ value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### PinInput
Secure multi-cell PIN and verification-code input with regex and paste support.
- Mount: `data-component="PinInput"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Verification code"`, `name: string = "pin"`, `value: string = ""`, `length: number = 4`, `pattern: string = "[0-9]"`, `type: string = "text"`, `inputMode: string = "numeric"`, `placeholder: string = "○"`, `autocomplete: string = "one-time-code"`, `masked: boolean = false`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `autoFocus: boolean = false`, `autoSubmit: boolean = false`, `allowPaste: boolean = true`, `clearable: boolean = true`, `clearLabel: string = "Clear code"`, `separator: string = ""`, `groupSize: number = 0`, `helpText: string = ""`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `paste({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### StrongPassword
Theme-aware, responsive strong password component.
- Mount: `data-component="StrongPassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `change({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `strength({ value: string; score: number; maximumScore: number; percent: number; level: string })`
### ToggleCount
Theme-aware, responsive toggle count component.
- Mount: `data-component="ToggleCount"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "segmented"`, `class: string = ""`, `name: string = "billing-cycle"`, `value: string = "monthly"`, `firstValue: string = "monthly"`, `firstLabel: string = "Monthly"`, `secondValue: string = "annual"`, `secondLabel: string = "Annual"`, `ariaLabel: string = "Billing frequency"`, `items: unknown[] = []`, `currency: string = "$"`, `suffix: string = ""`, `firstValueKey: string = "monthly"`, `secondValueKey: string = "annual"`, `emptyValue: string = "—"`, `align: string = "end"`, `fullWidth: boolean = true`, `disabled: boolean = false`, `animate: boolean = true`, `animationDuration: number = 450`, `animationSteps: number = 18`
- Slots: None
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### TogglePassword
Accessible password field with optional show and hide controls.
- Mount: `data-component="TogglePassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Enter your password"`, `autocomplete: string = "current-password"`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"`, `fields: unknown[] = []`, `visible: boolean = false`, `toggleable: boolean = true`, `toggleMode: string = "button"`, `checkboxLabel: string = "Show password"`, `showLabel: string = "Show password"`, `hideLabel: string = "Hide password"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `helpText: string = ""`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ name?: string; value: string | number | boolean | null | object; visible: boolean })`, `change({ name?: string; value: string | number | boolean | null | object; visible: boolean })`, `toggle({ visible: boolean })`
## Base
### Accordion
Theme-aware, responsive accordion component.
- Mount: `data-component="Accordion"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`, `id: string = "accordion"`, `items: unknown[] = []`, `defaultOpen: unknown[] = []`, `multiple: boolean = false`, `alwaysOpen: boolean = false`, `disabled: boolean = false`, `indicator: string = "plus"`, `indicatorPosition: string = "start"`, `showIndicator: boolean = true`, `bordered: boolean = false`, `separated: boolean = false`, `flush: boolean = false`, `contentItalic: boolean = false`
- Slots: None
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Alert
Theme-aware, responsive alert component.
- Mount: `data-component="Alert"`
- Props: `size: string = "default"`, `color: string = "info"`, `variant: string = "soft"`, `class: string = ""`, `radius: string = "md"`, `shadow: string = "sm"`, `title: string = "Alert"`, `description: string = ""`, `items: unknown[] = []`, `actions: unknown[] = []`, `showIcon: boolean = false`, `icon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss alert"`, `role: string = "alert"`, `live: string = "polite"`, `linkLabel: string = ""`, `linkHref: string = ""`, `actionLabel: string = ""`, `actionHref: string = ""`, `compact: boolean = false`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Avatar
Theme-aware, responsive avatar component.
- Mount: `data-component="Avatar"`
- Props: `src: string = ""`, `alt: string = ""`, `initials: string = ""`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "circle"`, `status: string = ""`, `statusLabel: string = ""`, `statusPosition: string = "bottom"`, `badge: string = ""`, `badgeIcon: string = ""`, `badgeLabel: string = ""`, `tooltip: string = ""`, `name: string = ""`, `description: string = ""`, `loading: string = "lazy"`, `class: string = ""`
- Slots: None
- Outputs: `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`, `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### AvatarGroup
Theme-aware, responsive avatar group component.
- Mount: `data-component="AvatarGroup"`
- Props: `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "circle"`, `layout: string = "stack"`, `maxVisible: number = 4`, `columns: number = 3`, `borderColor: string = ""`, `showTooltips: boolean = true`, `overflowLabel: string = "Show remaining members"`, `class: string = ""`
- Slots: None
- Outputs: `overflow({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Badge
Theme-aware, responsive badge component.
- Mount: `data-component="Badge"`
- Props: `label: string = "Badge"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "pill"`, `class: string = ""`, `icon: string = ""`, `iconPosition: string = "start"`, `dot: boolean = false`, `dotOnly: boolean = false`, `dotLabel: string = "Status"`, `animated: boolean = false`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Remove badge"`, `truncate: boolean = false`, `maxWidth: string = "12rem"`, `anchorLabel: string = ""`, `anchorIcon: string = ""`, `placement: string = "inline"`, `anchorLabelText: string = "Badge anchor"`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Blockquote
Theme-aware, responsive blockquote component.
- Mount: `data-component="Blockquote"`
- Props: `quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."`, `citation: string = ""`, `citationTitle: string = ""`, `citationUrl: string = ""`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `size: string = "md"`, `color: string = "primary"`, `align: string = "left"`, `variant: string = "default"`, `quoteMark: boolean = true`, `italic: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Button
Theme-aware, responsive button component.
- Mount: `data-component="Button"`
- Props: `label: string = "Button"`, `loadingLabel: string = "Loading…"`, `description: string = ""`, `as: string = ""`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `type: string = "button"`, `variant: string = "default"`, `color: string = "primary"`, `size: string = "default"`, `disabled: boolean = false`, `loading: boolean = false`, `pill: boolean = false`, `fullWidth: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `ariaLabel: string = ""`, `ariaPressed: string = ""`, `ariaExpanded: string = ""`, `ariaControls: string = ""`, `title: string = ""`, `autofocus: boolean = false`, `controlClass: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ButtonGroup
Theme-aware, responsive button group component.
- Mount: `data-component="ButtonGroup"`
- Props: `items: unknown[] = []`, `value: string = ""`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `orientation: string = "horizontal"`, `responsive: boolean = false`, `attached: boolean = true`, `selectable: boolean = false`, `toolbar: boolean = false`, `disabled: boolean = false`, `ariaLabel: string = "Button group"`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`, `change({ value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`
### Card
Group related content in a responsive themed surface with title, description, content, and supporting slots.
- Mount: `data-component="Card"`
- Props: `title: string = "Card title"`, `subtitle: string = ""`, `description: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `navigate({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### Carousel
Theme-aware, responsive carousel component.
- Mount: `data-component="Carousel"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = ""`, `description: string = ""`, `items: unknown[] = []`, `activeIndex: number = 0`, `slidesPerView: number = 1`, `gap: string = "0.75rem"`, `showPagination: boolean = false`, `isAutoPlay: boolean = false`, `autoplayInterval: number = 4000`, `isInfiniteLoop: boolean = false`, `isRTL: boolean = false`, `isCentered: boolean = false`, `isDraggable: boolean = false`, `isAutoHeight: boolean = false`, `isSnap: boolean = false`, `showCounter: boolean = false`, `thumbnails: string = "none"`, `ariaLabel: string = "Content carousel"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `initialize({ index: number; count: number })`, `change({ index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })`, `previous({ index: number; previousIndex: number; item: string | number | boolean | null | object })`, `next({ index: number; previousIndex: number; item: string | number | boolean | null | object })`, `play({ index: number; interval: number })`, `pause({ index: number })`, `reachStart({ index: number })`, `reachEnd({ index: number })`, `dragStart({ index: number; x: null })`, `dragEnd({ index: number; distance: number })`
### ChatBubble
Theme-aware, responsive chat bubble component.
- Mount: `data-component="ChatBubble"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = ""`, `description: string = ""`, `items: unknown[] = []`, `oneSided: boolean = false`, `showAvatars: boolean = false`, `showMetadata: boolean = false`, `ariaLabel: string = "Conversation"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `action({ action: boolean; item: string | number | boolean | null | object; index: number })`, `messageClick({ item: string | number | boolean | null | object; index: number; direction: string })`, `avatarClick({ item: string | number | boolean | null | object; index: number; direction: string })`, `linkClick({ link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`
### Collapse
Theme-aware, responsive collapse component.
- Mount: `data-component="Collapse"`
- Props: `size: string = "default"`, `color: string = "primary"`, `items: unknown[] = []`, `multiple: boolean = false`, `mode: string = "panel"`, `initialOpenIndexes: unknown[] = []`, `ariaLabel: string = "Collapsible content"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })`, `open({ index: number; item: string | number | boolean | null | object })`, `close({ index: number; item: string | number | boolean | null | object })`
### DatePicker
Theme-aware, responsive date picker component.
- Mount: `data-component="DatePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Date Picker"`, `id: string = ""`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "date"`, `locale: string = "en-US"`, `firstDayOfWeek: number = 0`, `months: string = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]`, `days: string = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]`, `years: string = [2024, 2025, 2026, 2027, 2028, 2029, 2030]`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `variant: string = "normal"`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `open({ value: string; name: string; sourceEvent: Event })`, `close({ value: string; name: string; sourceEvent: Event })`, `focus({ value: string; name: string; sourceEvent: Event })`, `blur({ value: string; name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
### DeviceFrame
Theme-aware, responsive device frame component.
- Mount: `data-component="DeviceFrame"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Device Frame"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `device: string = "phone"`, `orientation: string = "portrait"`, `src: string = ""`, `srcdoc: string = ""`, `frameTitle: string = "Device preview"`, `showToolbar: boolean = true`, `allow: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ device: string; orientation: string; sourceEvent: Event })`, `rotate({ device: string; orientation: string; sourceEvent: Event })`
### FileUploadProgress
Theme-aware, responsive file upload progress component.
- Mount: `data-component="FileUploadProgress"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Progress"`, `value: number = 50`, `max: number = 100`, `showValue: boolean = true`, `fileName: string = ""`, `fileSize: string = ""`, `uploadedSize: string = ""`, `status: string = "uploading"`, `cancelLabel: string = "Cancel upload"`, `retryLabel: string = "Retry upload"`, `class: string = ""`
- Slots: None
- Outputs: `cancel({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `retry({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `complete({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### LegendIndicator
Theme-aware, responsive legend indicator component.
- Mount: `data-component="LegendIndicator"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Legend Indicator"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### List
Present structured responsive linked or status items with icons, descriptions, actions, and selection events.
- Mount: `data-component="List"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### ListGroup
Theme-aware, responsive list group component.
- Mount: `data-component="ListGroup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List Group"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Marquee
Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior.
- Mount: `data-component="Marquee"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Progress
Theme-aware, responsive progress component.
- Mount: `data-component="Progress"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Progress"`, `value: number = 50`, `max: number = 100`, `showValue: boolean = true`, `class: string = ""`
- Slots: None
- Outputs: None
### Rating
Theme-aware, responsive rating component.
- Mount: `data-component="Rating"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Rating"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Skeleton
Theme-aware, responsive skeleton component.
- Mount: `data-component="Skeleton"`
- Props: `color: string = "primary"`, `label: string = "Loading"`, `size: string = "md"`, `lines: number = 3`, `class: string = ""`
- Slots: None
- Outputs: None
### Spinner
Theme-aware, responsive spinner component.
- Mount: `data-component="Spinner"`
- Props: `color: string = "primary"`, `label: string = "Loading"`, `size: string = "md"`, `lines: number = 3`, `class: string = ""`
- Slots: None
- Outputs: None
### StyledIcon
Theme-aware, responsive styled icon component.
- Mount: `data-component="StyledIcon"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Styled Icon"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Timeline
Present responsive chronological activity, milestones, or workflow status with rich item metadata.
- Mount: `data-component="Timeline"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Toast
Theme-aware, responsive toast component.
- Mount: `data-component="Toast"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Toast"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### TreeView
Theme-aware, responsive tree view component.
- Mount: `data-component="TreeView"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Tree View"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `expand({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `collapse({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Core
### AuthForm
Reusable auth form component.
- 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`
- Outputs: `submit({ event: Event; mode: string; action: string })`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### AuthSplitLayout
Reusable auth split layout component.
- 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: unknown[] = []`, `class: string = ""`
- Slots: `aside-extra`, `form`
- Outputs: None
### PortalDashboard
Reusable portal dashboard component.
- 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: unknown[] = []`, `actions: unknown[] = []`, `updates: unknown[] = []`, `tasks: unknown[] = []`, `class: string = ""`
- Slots: `hero-action`
- Outputs: `action({ item: string | number | boolean | null | object; value: string | number | boolean; index: number })`, `navigate({ item: string | number | boolean | null | object; value: string | number | boolean; index: number })`
### PreferenceSwitcher
Reusable preference switcher component.
- 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
- Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
## Data
### MetricCard
Display one operational metric with value, suffix, description, icon, trend, progress, and optional action.
- Mount: `data-component="MetricCard"`
- Props: `label: string = "Metric"`, `value: string = "0"`, `description: string = ""`, `icon: string = ""`, `iconStyle: string = "soft"`, `prefix: string = ""`, `suffix: string = ""`, `badge: string = ""`, `trend: string = ""`, `trendLabel: string = ""`, `trendDirection: string = "neutral"`, `progress: number = -1`, `progressLabel: string = ""`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = ""`, `actionIcon: string = ""`, `showArrow: boolean = true`, `selectable: boolean = false`, `disabled: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `class: string = ""`
- Slots: `default`, `footer`
- Outputs: `select({ label: string; value: string; href: string; sourceEvent: Event })`, `action({ label: string; value: string; href: string; sourceEvent: Event })`
### MetricGrid
Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid.
- Mount: `data-component="MetricGrid"`
- Props: `items: unknown[] = []`, `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"`, `maxWidth: string = "full"`, `minItemWidth: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })`
### StatsBar
Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
- Mount: `data-component="StatsBar"`
- Props: `items: unknown[] = []`, `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
- Outputs: None
## Forms
### 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: unknown[] = []`, `options: unknown[] = []`, `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: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ColorPicker
Theme-aware, responsive color picker component.
- Mount: `data-component="ColorPicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Color"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = "#2563eb"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### FileInput
Theme-aware, responsive file input component.
- Mount: `data-component="FileInput"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "File"`, `hiddenLabel: boolean = false`, `placeholder: string = "Choose a file"`, `value: string = ""`, `icon: string = "icon-[lucide--upload]"`, `iconPosition: string = "start"`, `accept: string = ""`, `multiple: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ files: File[]; name: string; sourceEvent: Event })`, `change({ files: File[]; name: string; sourceEvent: Event })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ files: File[]; name: string; sourceEvent: Event })`, `clear({ name: string; sourceEvent: Event })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Input
Theme-aware, responsive input component.
- Mount: `data-component="Input"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `type: string = "text"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `autocomplete: string = ""`, `inputmode: string = ""`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = ""`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `blur({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`, `keydown({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `keyup({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`
### InputGroup
Theme-aware, responsive input group component.
- Mount: `data-component="InputGroup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input group"`, `hiddenLabel: boolean = false`, `value: string = ""`, `placeholder: string = ""`, `type: string = "text"`, `startText: string = ""`, `endText: string = ""`, `icon: string = ""`, `iconPosition: string = "start"`, `actionLabel: string = ""`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `submit({ value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `action({ name: string; sourceEvent: Event })`
### Radio
Theme-aware, responsive radio component.
- Mount: `data-component="Radio"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Radio"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `options: unknown[] = []`, `checked: 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
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### RangeSlider
Theme-aware, responsive range slider component.
- Mount: `data-component="RangeSlider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Range"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: number = 50`, `min: number = 0`, `max: number = 100`, `step: number = 1`, `showValue: boolean = true`, `showBounds: boolean = true`, `showSteps: boolean = false`, `marks: unknown[] = []`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input(({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))`, `change(({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### SearchBox
Provide an accessible responsive search field with labels, validation states, sizes, and input or change events.
- Mount: `data-component="SearchBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: None
### Select
Theme-aware, responsive select component.
- Mount: `data-component="Select"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Select"`, `hiddenLabel: boolean = false`, `value: string = ""`, `values: unknown[] = []`, `options: unknown[] = []`, `placeholder: string = "Select an option"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `multiple: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `open({ name: string; sourceEvent: Event })`, `close({ name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
### Switch
Theme-aware, responsive switch component.
- Mount: `data-component="Switch"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Switch"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `checked: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Textarea
Theme-aware, responsive textarea component.
- Mount: `data-component="Textarea"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Textarea"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `rows: number = 5`, `resize: string = "vertical"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `minlength: string = ""`, `maxlength: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`
### TimePicker
Theme-aware, responsive time picker component.
- Mount: `data-component="TimePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Time"`, `hiddenLabel: boolean = false`, `value: string = ""`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = "icon-[lucide--clock-3]"`, `iconPosition: string = "end"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `format: string = "24"`, `minuteStep: number = 5`, `hours: string = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]`, `minutes: string = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"]`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ value: string; name: string; sourceEvent: Event })`, `blur({ value: string; name: string; sourceEvent: Event })`, `open({ value: string; name: string; sourceEvent: Event })`, `close({ value: string; name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
## Integrations
### AdvancedDatePicker
Theme-aware, responsive advanced date picker component.
- Mount: `data-component="AdvancedDatePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Advanced Date Picker"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### AdvancedRangeSlider
Theme-aware, responsive advanced range slider component.
- Mount: `data-component="AdvancedRangeSlider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Advanced Range Slider"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `start({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `end({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Chart
Theme-aware, responsive chart component.
- Mount: `data-component="Chart"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Chart"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `dataPointClick({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `legendToggle({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Clipboard
Theme-aware, responsive clipboard component.
- Mount: `data-component="Clipboard"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Clipboard"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `copy({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### Confetti
Theme-aware, responsive confetti component.
- Mount: `data-component="Confetti"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Confetti"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `start({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### DataMap
Theme-aware, responsive data map component.
- Mount: `data-component="DataMap"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Data Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### DataTable
Theme-aware, responsive data table component.
- Mount: `data-component="DataTable"`
- Props: `size: string = "default"`, `color: string = "primary"`, `caption: string = "Data Table"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `striped: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: `sort({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `rowClick({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `pageChange({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### DragAndDrop
Theme-aware, responsive drag and drop component.
- Mount: `data-component="DragAndDrop"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Drag And Drop"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `dragStart({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragEnd({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragEnter({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragLeave({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `drop({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### FileUpload
Theme-aware, responsive file upload component.
- Mount: `data-component="FileUpload"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "File Upload"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `upload({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `progress({ progress: number; [key: string]: string | number | boolean | null | object } | number)`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`, `cancel({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `remove({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Map
Present responsive location information and markers with map-ready metadata and movement or marker events.
- Mount: `data-component="Map"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### ToastNotifications
Theme-aware, responsive toast notifications component.
- Mount: `data-component="ToastNotifications"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Toast Notifications"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `add({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### WysiwygEditor
Theme-aware, responsive wysiwyg editor component.
- Mount: `data-component="WysiwygEditor"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Wysiwyg Editor"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Layout
### Columns
Create responsive balanced content columns with configurable count, gap, density, and maximum width.
- Mount: `data-component="Columns"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Container
Constrain and align page content with responsive gutters and compact, wide, or full width options.
- Mount: `data-component="Container"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### CustomScrollbar
Theme-aware, responsive custom scrollbar component.
- Mount: `data-component="CustomScrollbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: `scroll({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Divider
Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.
- Mount: `data-component="Divider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: None
- Outputs: None
### FeatureGrid
Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid.
- Mount: `data-component="FeatureGrid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `items: unknown[] = []`, `columns: number = 3`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `minItemWidth: string = ""`, `equalHeight: boolean = true`, `align: string = "stretch"`, `maxWidth: string = "full"`, `variant: string = "default"`, `label: string = "Features"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Footer
Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
- Mount: `data-component="Footer"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Grid
Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.
- Mount: `data-component="Grid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Image
Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.
- Mount: `data-component="Image"`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `rounded: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Kbd
Theme-aware, responsive kbd component.
- Mount: `data-component="Kbd"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "⌘ K"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### LayoutSplitter
Theme-aware, responsive layout splitter component.
- Mount: `data-component="LayoutSplitter"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: `resizeStart({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resize({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resizeEnd({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Link
Render an accessible internal or external link with target, relation, size, color, and public focus or click events.
- Mount: `data-component="Link"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### PublicPageShell
Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public 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`
- Outputs: None
### Section
Create a responsive themed page section with controlled spacing, width, borders, and surface treatment.
- 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`
- Outputs: None
### SectionHeader
Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy.
- 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`
- Outputs: None
### Typography
Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy.
- Mount: `data-component="Typography"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
## Marketing
### AnnouncementBar
Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
- 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"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### CTASection
Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
- 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 = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
- Slots: `default`, `actions`, `visual`, `footer`
- Outputs: None
### FeatureCard
Present one linked feature or service with media, icon, badge, description, and action.
- Mount: `data-component="FeatureCard"`
- Props: `icon: string = ""`, `iconStyle: string = "soft"`, `iconSize: string = "default"`, `eyebrow: string = ""`, `title: string = "Feature"`, `description: string = ""`, `image: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `imageAspect: string = "wide"`, `imageLoading: string = "lazy"`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = "Learn more"`, `actionIcon: string = ""`, `showArrow: boolean = true`, `stretchedLink: boolean = true`, `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: `media`, `icon`, `default`, `footer`
- Outputs: None
### FeatureIconCard
Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
- 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`
- Outputs: None
### Hero
Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel.
- Mount: `data-component="Hero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = ""`, `icon: string = ""`, `title: string = "Build something remarkable"`, `highlight: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "split"`, `visualPosition: string = "right"`, `visualStyle: string = "plain"`, `showDecorations: boolean = true`, `fullBleed: boolean = true`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualIcon: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualItems: unknown[] = []`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `primaryTarget: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `secondaryTarget: string = ""`, `tertiaryLabel: string = ""`, `tertiaryHref: string = ""`, `tertiaryIcon: string = ""`, `tertiaryTarget: string = ""`, `badges: unknown[] = []`, `trustItems: unknown[] = []`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `eyebrow`, `actions`, `trust`, `default`, `visual`, `footer`
- Outputs: None
### HeroActions
Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking.
- Mount: `data-component="HeroActions"`
- Props: `actions: unknown[] = []`, `align: string = "left"`, `orientation: string = "horizontal"`, `stackOnMobile: boolean = true`, `fullWidthMobile: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### MarketingSectionHeader
Introduce marketing content with an eyebrow, title, description, and 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`
- Outputs: None
### PageHeader
Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions.
- Mount: `data-component="PageHeader"`
- Props: `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `id: string = "page-title"`, `align: string = "left"`, `centered: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `maxWidth: string = "xl"`, `showBreadcrumbs: boolean = false`, `breadcrumbs: unknown[] = []`, `breadcrumbParent: string = ""`, `breadcrumbParentHref: string = ""`, `breadcrumbCurrent: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `color: string = "primary"`, `variant: string = "default"`, `borderBottom: boolean = true`, `class: string = ""`
- Slots: `meta`, `actions`, `default`
- Outputs: None
### SplitHero
Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
- Mount: `data-component="SplitHero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `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"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
- Slots: `default`, `actions`, `trust`, `visual`
- Outputs: None
### TextLink
Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling.
- 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
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Navigation
### BackToTop
Provide a responsive floating control that returns long pages to the top and can show scroll progress.
- 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`, `showLabel: boolean = false`, `alwaysVisible: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "round"`, `class: string = ""`
- Slots: None
- Outputs: None
### Breadcrumb
Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
- Mount: `data-component="Breadcrumb"`
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
- Slots: None
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### MegaMenu
Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Mega Menu"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Nav
Theme-aware, responsive nav component.
- Mount: `data-component="Nav"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Nav"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Navbar
Theme-aware, responsive navbar component.
- Mount: `data-component="Navbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
- Slots: `topbar`, `actions`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`, `action({ item: string | number | boolean | null | object; value: string | number | boolean })`
### Pagination
Theme-aware, responsive pagination component.
- Mount: `data-component="Pagination"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Pagination"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Scrollspy
Theme-aware, responsive scrollspy component.
- Mount: `data-component="Scrollspy"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Scrollspy"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Sidebar
Theme-aware, responsive sidebar component.
- Mount: `data-component="Sidebar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mobileLabel: string = "Open navigation"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ source: string })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`
### Stepper
Theme-aware, responsive stepper component.
- Mount: `data-component="Stepper"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Stepper"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Tabs
Switch between related responsive content panels with horizontal or vertical orientation and selection events.
- Mount: `data-component="Tabs"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Tabs"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: None
## Overlays
### ContextMenu
Open an accessible keyboard-aware action menu from pointer or keyboard context interactions.
- Mount: `data-component="ContextMenu"`
- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `trigger: string = "contextmenu"`, `placement: string = "pointer"`, `align: string = "start"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `title: string = ""`, `description: string = ""`, `label: string = "Context menu"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `minWidth: string = "14rem"`, `maxWidth: string = "20rem"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ x: number; y: number; trigger: string; sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `select({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`
### Drawer
Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events.
- Mount: `data-component="Drawer"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ placement: string; sourceEvent: Event })`, `close({ reason: string; placement: string; sourceEvent: Event })`, `cancel({ placement: string; sourceEvent: Event })`
### Dropdown
Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement.
- Mount: `data-component="Dropdown"`
- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `label: string = "Open menu"`, `icon: string = ""`, `showChevron: boolean = true`, `menuLabel: string = "Dropdown menu"`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `emptyLabel: string = "No menu items"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `toggle({ open: boolean; sourceEvent: Event; reason?: object })`, `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `select({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`
### Modal
Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing.
- Mount: `data-component="Modal"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `cancel({ sourceEvent: Event })`, `confirm({ sourceEvent: Event })`
### Popover
Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events.
- Mount: `data-component="Popover"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `triggerLabel: string = "Open popover"`, `triggerIcon: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `showArrow: boolean = true`, `showClose: boolean = false`, `closeLabel: string = "Close popover"`, `closeOnOutside: boolean = true`, `closeOnEscape: boolean = true`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `closeOnAction: boolean = true`, `disabled: boolean = false`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `toggle({ open: boolean; sourceEvent: Event; reason?: object })`, `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `action({ href: string; sourceEvent: Event })`
### Tooltip
Show concise accessible contextual help on hover, focus, click, or controlled open state.
- Mount: `data-component="Tooltip"`
- Props: `id: string = ""`, `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = ""`, `description: string = ""`, `content: string = "Tooltip"`, `trigger: string = "hover"`, `placement: string = "top"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "dark"`, `maxWidth: string = "18rem"`, `offset: number = 10`, `showArrow: boolean = true`, `interactive: boolean = false`, `disabled: boolean = false`, `class: string = ""`
- Slots: `trigger`, `default`
- Outputs: `toggle({ open: boolean; reason: string; sourceEvent: Event })`, `open({ reason: string; sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`
## Tables
### Table
Theme-aware, responsive table component.
- Mount: `data-component="Table"`
- Props: `size: string = "default"`, `color: string = "primary"`, `caption: string = "Table"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `striped: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: `sort({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `rowClick({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
+80 -12
View File
@@ -15,6 +15,11 @@ driven by `var(--wire-*)` theme tokens, so components restyle instantly when the
theme changes. The tiny JS surface (`src/index.ts`) exists only so the toolchain theme changes. The tiny JS surface (`src/index.ts`) exists only so the toolchain
(CLI build + dev server) can locate the component directory and stylesheet. (CLI build + dev server) can locate the component directory and stylesheet.
The complete PDF-aligned catalog currently contains **85 components**. The
generated `COMPONENTS.md` and `component-reference.json` files document every
mount name, prop, inferred type, default/required status, slot, event, category,
and source file directly from the packaged `.wrn` source.
## Installation ## Installation
```bash ```bash
@@ -30,10 +35,11 @@ In practice you rarely install this directly: `@wrnexus/cli` and
## Components ## Components
Components live as `.wrn` files under `packages/ui/components/`. The mount name Components live as `.wrn` files under `packages/ui/components/`. The canonical mount
is the **lowercase file basename** (e.g. `button.wrn``data-component="button"`). name comes from the component declaration (for example, `component Button` mounts as
Each accepts a `class` prop (appended to its root element) and most render their `data-component="Button"`). Component lookup is case-insensitive, so existing lowercase
body from either a named prop or the default slot. mounts continue to work. Each component accepts a `class` prop (appended to its root
element), and most render their body from either a named prop or the default slot.
### Layout ### Layout
@@ -65,23 +71,38 @@ body from either a named prop or the default slot.
### Additional controls & data display ### Additional controls & data display
Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`, Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`,
`tooltip`, and `table`. `tooltip`, `table`, `FAQAccordion`, `AnnouncementBar`, and `BackToTop`.
The PDF-defined minimum release and essential build-first set also includes
typed typography, form primitives, loading actions, combobox and multi-select,
time/date-time and recurring schedule controls, confirmation dialogs, data
tables, filters, desktop/mobile navigation, mega menus, marketing/product/legal
page shells, product and metric cards, FAQ composition, pricing comparison,
SDK tabs, legal navigation, and cookie preferences.
`Seo` and `StructuredData` remain framework/page concerns rather than body
components: use the native page `seo { ... }` block and document-head APIs so
metadata is emitted in `<head>` instead of invalid component markup.
The authoritative, always-current list is `uiComponentNames()` (below), which reads The authoritative, always-current list is `uiComponentNames()` (below), which reads
the component directory at runtime. the component directory at runtime.
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
equivalent is exported as `@wrnexus/ui/component-reference.json`.
## API ## API
The JS module (`@wrnexus/ui`) exposes four helpers used by the build tooling to The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
locate the component assets. There is no component code to import — the components locate the component assets. There is no component code to import — the components
are `.wrn` files rendered server-side. are `.wrn` files rendered server-side.
| Export | Signature | Returns | | Export | Signature | Returns |
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------ | | ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). | | `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). |
| `uiCssPath` | `() => string` | Absolute path to `ui.css`. | | `uiCssPath` | `() => string` | Absolute path to `ui.css`. |
| `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). | | `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). |
| `uiComponentNames` | `() => string[]` | Sorted list of built-in component names (e.g. for `wrnexus eject` listing). | | `uiComponentNames` | `() => string[]` | Sorted list of declared built-in component names. |
| `uiComponentPath` | `(name: string) => string` | Absolute source path for a declared component name or case-insensitive alias. |
### `./ui.css` asset export ### `./ui.css` asset export
@@ -97,6 +118,53 @@ are `.wrn` files rendered server-side.
The framework serves this stylesheet once at `/__wrnexus/ui.css`, so pages get all The framework serves this stylesheet once at `/__wrnexus/ui.css`, so pages get all
component styles from a single request. component styles from a single request.
### Tailwind and motion
Components use static Tailwind utility classes alongside the shared `.wire-*`
layer. If the package is consumed by a separate Tailwind build, include its
component sources so every utility is generated:
```css
@import "tailwindcss";
@source "../node_modules/@wrnexus/ui/components/*.wrn";
```
The shared stylesheet gives all component boundaries consistent, GPU-friendly
entry and interaction motion. Override `--wire-motion-fast`,
`--wire-motion-base`, `--wire-motion-slow`, `--wire-ease-standard`, or
`--wire-ease-emphasized` to tune it. Hover lift is limited to precise pointing
devices and `prefers-reduced-motion` is honored automatically.
### Using the selected theme in application UI
The active theme and palette are not limited to `@wrnexus/ui` components. The
framework exposes the resolved values as semantic CSS custom properties, so
pages and custom `.wrn` components can use the same contract:
```css
.account-card {
background: var(--wire-color-surface);
color: var(--wire-color-text);
border: 1px solid var(--wire-color-border);
}
.account-card__action {
background: var(--wire-color-primary);
color: var(--wire-color-primary-contrast);
}
```
Stable no-spacing helper classes are also available: `wire-bg-page`,
`wire-bg-surface`, `wire-bg-surface-2`, `wire-bg-primary`, `wire-bg-secondary`,
`wire-text`, `wire-text-muted`, `wire-text-primary`, `wire-text-success`,
`wire-text-warning`, `wire-text-danger`, and `wire-border`.
Tailwind-authored custom markup can continue using the palette families already
used by packaged components. `indigo-*` and `violet-*` resolve to primary,
`blue-*` to info, `emerald-*`/`green-*` to success, `amber-*` to warning, and
`red-*`/`rose-*` to danger. These aliases live at `:root`, so they work outside
a `[data-component]` boundary too.
## Usage ## Usage
### Auto-discovery ### Auto-discovery
+545
View File
@@ -0,0 +1,545 @@
{
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
"components": [
{
"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."
},
{
"name": "Alert",
"category": "base",
"purpose": "Theme-aware, responsive alert component."
},
{
"name": "AnnouncementBar",
"category": "marketing",
"purpose": "Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls."
},
{
"name": "AuthForm",
"category": "core",
"purpose": "Reusable auth form component."
},
{
"name": "AuthSplitLayout",
"category": "core",
"purpose": "Reusable auth split layout component."
},
{
"name": "Avatar",
"category": "base",
"purpose": "Theme-aware, responsive avatar component."
},
{
"name": "AvatarGroup",
"category": "base",
"purpose": "Theme-aware, responsive avatar group component."
},
{
"name": "BackToTop",
"category": "navigation",
"purpose": "Provide a responsive floating control that returns long pages to the top and can show scroll progress."
},
{
"name": "Badge",
"category": "base",
"purpose": "Theme-aware, responsive badge component."
},
{
"name": "Blockquote",
"category": "base",
"purpose": "Theme-aware, responsive blockquote component."
},
{
"name": "Breadcrumb",
"category": "navigation",
"purpose": "Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events."
},
{
"name": "Button",
"category": "base",
"purpose": "Theme-aware, responsive button component."
},
{
"name": "ButtonGroup",
"category": "base",
"purpose": "Theme-aware, responsive button group component."
},
{
"name": "CTASection",
"category": "marketing",
"purpose": "Close a page or major section with conversion-focused copy, actions, and optional supporting visual content."
},
{
"name": "Card",
"category": "base",
"purpose": "Group related content in a responsive themed surface with title, description, content, and supporting slots."
},
{
"name": "Carousel",
"category": "base",
"purpose": "Theme-aware, responsive carousel component."
},
{
"name": "Chart",
"category": "integrations",
"purpose": "Theme-aware, responsive chart component."
},
{
"name": "ChatBubble",
"category": "base",
"purpose": "Theme-aware, responsive chat bubble component."
},
{
"name": "Checkbox",
"category": "forms",
"purpose": "Theme-aware, responsive checkbox component."
},
{
"name": "Clipboard",
"category": "integrations",
"purpose": "Theme-aware, responsive clipboard component."
},
{
"name": "Collapse",
"category": "base",
"purpose": "Theme-aware, responsive collapse component."
},
{
"name": "ColorPicker",
"category": "forms",
"purpose": "Theme-aware, responsive color picker component."
},
{
"name": "Columns",
"category": "layout",
"purpose": "Create responsive balanced content columns with configurable count, gap, density, and maximum width."
},
{
"name": "ComboBox",
"category": "advanced-forms",
"purpose": "Editable autocomplete combobox with local and remote suggestions."
},
{
"name": "Confetti",
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component."
},
{
"name": "Container",
"category": "layout",
"purpose": "Constrain and align page content with responsive gutters and compact, wide, or full width options."
},
{
"name": "ContextMenu",
"category": "overlays",
"purpose": "Open an accessible keyboard-aware action menu from pointer or keyboard context interactions."
},
{
"name": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component."
},
{
"name": "CustomScrollbar",
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component."
},
{
"name": "DataMap",
"category": "integrations",
"purpose": "Theme-aware, responsive data map component."
},
{
"name": "DataTable",
"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": "Separate related horizontal or vertical content with optional labels, sizes, and semantic colors."
},
{
"name": "DragAndDrop",
"category": "integrations",
"purpose": "Theme-aware, responsive drag and drop component."
},
{
"name": "Drawer",
"category": "overlays",
"purpose": "Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events."
},
{
"name": "Dropdown",
"category": "overlays",
"purpose": "Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement."
},
{
"name": "FeatureCard",
"category": "marketing",
"purpose": "Present one linked feature or service with media, icon, badge, description, and action."
},
{
"name": "FeatureGrid",
"category": "layout",
"purpose": "Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid."
},
{
"name": "FeatureIconCard",
"category": "marketing",
"purpose": "Present a compact feature or benefit with a styled icon, title, description, badge, and optional link."
},
{
"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": "layout",
"purpose": "Render structured responsive footer navigation, pre and post content, copyright content, links, and public events."
},
{
"name": "Grid",
"category": "layout",
"purpose": "Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width."
},
{
"name": "Hero",
"category": "marketing",
"purpose": "Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel."
},
{
"name": "HeroActions",
"category": "marketing",
"purpose": "Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking."
},
{
"name": "Image",
"category": "layout",
"purpose": "Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment."
},
{
"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": "Render an accessible internal or external link with target, relation, size, color, and public focus or click events."
},
{
"name": "List",
"category": "base",
"purpose": "Present structured responsive linked or status items with icons, descriptions, actions, and selection events."
},
{
"name": "ListGroup",
"category": "base",
"purpose": "Theme-aware, responsive list group component."
},
{
"name": "Map",
"category": "integrations",
"purpose": "Present responsive location information and markers with map-ready metadata and movement or marker events."
},
{
"name": "MarketingSectionHeader",
"category": "marketing",
"purpose": "Introduce marketing content with an eyebrow, title, description, and optional linked action."
},
{
"name": "Marquee",
"category": "base",
"purpose": "Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior."
},
{
"name": "MegaMenu",
"category": "navigation",
"purpose": "Theme-aware, responsive mega menu component."
},
{
"name": "MetricCard",
"category": "data",
"purpose": "Display one operational metric with value, suffix, description, icon, trend, progress, and optional action."
},
{
"name": "MetricGrid",
"category": "data",
"purpose": "Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid."
},
{
"name": "Modal",
"category": "overlays",
"purpose": "Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing."
},
{
"name": "Nav",
"category": "navigation",
"purpose": "Theme-aware, responsive nav component."
},
{
"name": "Navbar",
"category": "navigation",
"purpose": "Theme-aware, responsive navbar component."
},
{
"name": "PageHeader",
"category": "marketing",
"purpose": "Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary 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."
},
{
"name": "Popover",
"category": "overlays",
"purpose": "Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events."
},
{
"name": "PortalDashboard",
"category": "core",
"purpose": "Reusable portal dashboard component."
},
{
"name": "PreferenceSwitcher",
"category": "core",
"purpose": "Reusable preference switcher component."
},
{
"name": "Progress",
"category": "base",
"purpose": "Theme-aware, responsive progress component."
},
{
"name": "PublicPageShell",
"category": "layout",
"purpose": "Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages."
},
{
"name": "Radio",
"category": "forms",
"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": "forms",
"purpose": "Provide an accessible responsive search field with labels, validation states, sizes, and input or change events."
},
{
"name": "Section",
"category": "layout",
"purpose": "Create a responsive themed page section with controlled spacing, width, borders, and surface treatment."
},
{
"name": "SectionHeader",
"category": "layout",
"purpose": "Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy."
},
{
"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": "marketing",
"purpose": "Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel."
},
{
"name": "StatsBar",
"category": "data",
"purpose": "Present a compact responsive strip of key facts, counts, performance indicators, or trust signals."
},
{
"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": "Switch between related responsive content panels with horizontal or vertical orientation and selection events."
},
{
"name": "TextLink",
"category": "marketing",
"purpose": "Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling."
},
{
"name": "Textarea",
"category": "forms",
"purpose": "Theme-aware, responsive textarea component."
},
{
"name": "TimePicker",
"category": "forms",
"purpose": "Theme-aware, responsive time picker component."
},
{
"name": "Timeline",
"category": "base",
"purpose": "Present responsive chronological activity, milestones, or workflow status with rich item metadata."
},
{
"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."
},
{
"name": "Tooltip",
"category": "overlays",
"purpose": "Show concise accessible contextual help on hover, focus, click, or controlled open state."
},
{
"name": "TreeView",
"category": "base",
"purpose": "Theme-aware, responsive tree view component."
},
{
"name": "Typography",
"category": "layout",
"purpose": "Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy."
},
{
"name": "WysiwygEditor",
"category": "integrations",
"purpose": "Theme-aware, responsive wysiwyg editor component."
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"generatedFrom": "full component catalog reset",
"removedCount": 0,
"replacements": {}
}
File diff suppressed because it is too large Load Diff
+206
View File
@@ -0,0 +1,206 @@
component Accordion {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
variant: string = "default"
class: string = ""
id: string = "accordion"
items: unknown[] = []
defaultOpen: unknown[] = []
multiple: boolean = false
alwaysOpen: boolean = false
disabled: boolean = false
indicator: string = "plus"
indicatorPosition: string = "start"
showIndicator: boolean = true
bordered: boolean = false
separated: boolean = false
flush: boolean = false
contentItalic: boolean = false
}
state openValues = defaultOpen
functions {
shared function itemValue(item, index) {
return item.value !== undefined && item.value !== ""
? String(item.value)
: String(index)
}
shared function nestedValue(parent, parentIndex, item, index) {
return itemValue(parent, parentIndex) + "." + itemValue(item, index)
}
shared function isOpen(value) {
return openValues.includes(value)
}
shared function allowsMultiple() {
return multiple || alwaysOpen
}
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-accordion]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent(eventName, true, false, {
component: "Accordion",
value: value,
item: item,
open: isOpen(value),
openValues: openValues
})
root.dispatchEvent(customEvent)
}
client function toggleItem(sourceEvent, value, item, wasOpen) {
if (disabled || item.disabled) {
return
}
wasOpen = isOpen(value)
if (wasOpen) {
openValues = openValues.filter((entry) => entry !== value)
} else if (allowsMultiple()) {
openValues = openValues.concat([value])
} else {
openValues = [value]
}
dispatchAccordionEvent(
sourceEvent,
wasOpen ? "close" : "open",
value,
item
)
dispatchAccordionEvent(sourceEvent, "change", value, item)
}
}
view {
<div
{...attrs}
id="{id}"
data-wrn-accordion
data-variant="{variant}"
data-indicator="{indicator}"
data-multiple="{allowsMultiple() ? 'true' : 'false'}"
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--accordion wire-next--accordion-{variant} {bordered ? 'wire-next--accordion-bordered' : ''} {separated ? 'wire-next--accordion-separated' : ''} {flush ? 'wire-next--accordion-flush' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
>
{#each items as item, index}
<section
class="wire-next__accordion-item"
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
>
<h3 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}"
aria-expanded="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}"
disabled="{disabled || item.disabled}"
@click="toggleItem(event, itemValue(item, index), item)"
>
{#if showIndicator && indicatorPosition === "start"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{item.label || item.title}</span>
{#if showIndicator && indicatorPosition === "end"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
</button>
</h3>
<div
id="{id}-panel-{index}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}"
aria-hidden="{isOpen(itemValue(item, index)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
{#if item.content}<p>{item.content}</p>{/if}
{#if item.children && item.children.length > 0}
<div class="wire-next__accordion-nested">
{#each item.children as child, childIndex}
<section
class="wire-next__accordion-item"
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
>
<h4 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}-{childIndex}"
aria-expanded="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}-{childIndex}"
disabled="{disabled || child.disabled}"
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
>
{#if showIndicator}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{child.label || child.title}</span>
</button>
</h4>
<div
id="{id}-panel-{index}-{childIndex}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}-{childIndex}"
aria-hidden="{isOpen(nestedValue(item, index, child, childIndex)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
<p>{child.content}</p>
</div>
</div>
</div>
</section>
{/each}
</div>
{/if}
</div>
</div>
</div>
</section>
{/each}
</div>
}
}
@@ -0,0 +1,27 @@
component AdvancedDatePicker {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Advanced Date Picker"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-date-picker wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,26 @@
component AdvancedRangeSlider {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
end(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Advanced Range Slider"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-range-slider wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
+459
View File
@@ -0,0 +1,459 @@
component AdvancedSelect {
outputs {
search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Advanced Select"
name: string = ""
value: string = ""
values: unknown[] = []
options: unknown[] = []
groups: unknown[] = []
placeholder: string = "Select an option"
placeholderIcon: string = ""
searchPlaceholder: string = "Search options…"
multiple: boolean = false
searchable: boolean = true
defaultOpen: boolean = false
clearable: boolean = true
allowEmpty: boolean = true
tags: boolean = false
disabled: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
helpText: string = ""
loading: boolean = false
loadingLabel: string = "Loading options…"
emptyLabel: string = "No options found"
selectedOptionsLabel: string = "Selected options"
clearLabel: string = "Clear selection"
createLabel: string = "Create"
loadMoreLabel: string = "Load more"
searchMode: string = "contains"
searchFields: string = "label,description"
minSearchLength: number = 0
searchResultLimit: number = 0
maxSelections: number = 0
showCounter: boolean = false
counterTemplate: string = "{selected} selected"
optionTemplate: string = "default"
selectedTemplate: string = "default"
closeOnSelect: boolean = true
scrollToSelected: boolean = true
fixed: boolean = false
placement: string = "bottom"
remote: boolean = false
remoteUrl: string = ""
remoteQueryParam: string = "q"
remoteDebounce: number = 250
remoteAutoLoad: boolean = true
infinite: boolean = false
hasMore: boolean = false
page: number = 1
class: string = ""
}
state open = defaultOpen
state query: string = ""
state activeIndex: number = -1
state selectedValue = value
state selectedValues = values
functions {
shared function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
shared function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
shared function matches(option) {
if (!query || query.length < Number(minSearchLength || 0)) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
shared function matchingOptions(list) {
return list.filter((option) => matches(option))
}
shared function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, Number(searchResultLimit))
}
return matchingOptions(list)
}
shared function flatVisibleOptions() {
return visibleOptions(allOptions())
}
shared function isSelected(option) {
return (
multiple
? selectedValues.includes(option.value)
: selectedValue === option.value
)
}
shared function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
shared function selectedCount() {
return multiple ? selectedValues.length : selectedValue ? 1 : 0
}
shared function counterText() {
return (
maxSelections
? selectedCount() + " / " + maxSelections + " selected"
: selectedCount() + " selected"
)
}
shared function selectedText() {
return (
multiple
? selectedValues.join(", ")
: selectedOptions().length
? selectedOptions()[0].label
: ""
)
}
shared function triggerText() {
return selectedCount() ? selectedText() : placeholder
}
shared function canSelect(option) {
if (option.disabled) {
return false
}
if (!multiple || isSelected(option)) {
return true
}
if (maxSelections <= 0) {
return true
}
return selectedCount() < maxSelections
}
shared function chooseSingle(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = ""
if (closeOnSelect) {
open = false
}
}
shared function chooseMultiple(option) {
if (!canSelect(option)) {
return
}
if (isSelected(option)) {
selectedValues = selectedValues.filter((item) => item !== option.value)
} else {
selectedValues = selectedValues.concat([option.value])
}
query = ""
}
shared function chooseOption(option) {
if (multiple) {
chooseMultiple(option)
return
}
chooseSingle(option)
}
client function clearSelection(event) {
event.stopPropagation()
selectedValue = ""
selectedValues = []
query = ""
open = false
}
shared function toggle() {
if (disabled) {
return;
};
open = !open;
activeIndex = open && flatVisibleOptions().length ? 0 : -1;
}
shared function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
client function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(-1)
} else if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
if (!open) {
open = true
} else if (activeIndex >= 0) {
chooseOption(flatVisibleOptions()[activeIndex])
}
} else if (event.key === "Escape") {
open = false
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
shared function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-select {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
>
<div class="wire-next__row">
<label id="{name}-label" for="{name}-trigger">{label}</label>
<small data-show="showCounter" data-text="counterText()">{counterText()}</small>
</div>
<div class="wire-next__select-control">
<button
{...attrs}
id="{name}-trigger"
type="button"
class="wire-next__select-trigger"
role="combobox"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
@click="toggle()"
@keydown="handleKeydown(event)"
>
<span class="wire-next__select-value">
<span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
<span
data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
data-text="selectedCount() + ' selected'"
>{selectedCount()} selected</span>
<span
data-show="multiple && selectedTemplate === 'text' && selectedCount() > 0"
data-text="selectedText()"
>{selectedText()}</span>
<span
class="wire-next__select-tags"
aria-label="{selectedOptionsLabel}"
data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
>
{#each allOptions() as option}
<span data-show="isSelected(option)">
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
{option.label}
</span>
{/each}
</span>
<span data-show="multiple && selectedCount() === 0">
{#if placeholderIcon}<i class="{placeholderIcon}" aria-hidden="true"></i>{/if}
<span class="wire-next__placeholder">{placeholder}</span>
</span>
</span>
<span
class="wire-next__select-chevron icon-[lucide--chevrons-up-down]"
aria-hidden="true"
></span>
</button>
<button
type="button"
class="wire-next__clear-select"
data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearSelection(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
</div>
<div
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
data-show="open"
style="{open ? '' : 'display: none'}"
>
{#if searchable}
<label class="wire-next__select-search">
<span class="wire-visually-hidden">{searchPlaceholder}</span>
<span class="wire-next__search-icon" aria-hidden="true">⌕</span>
<input
type="search"
value="{query}"
placeholder="{searchPlaceholder}"
autocomplete="off"
@input="query = event.target.value; activeIndex = flatVisibleOptions().length ? 0 : -1"
@keydown="handleKeydown(event)"
/>
</label>
{/if}
<div
class="wire-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<div class="wire-next__select-message" data-show="loading" role="status">
<i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
role="listbox"
aria-multiselectable="{multiple}"
>
{#each groups as group}
{#if visibleOptions(group.options || []).length}
<div class="wire-next__option-group" role="group" aria-label="{group.label}">
<div class="wire-next__group-label">{group.label}</div>
{#each group.options || [] as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i
class="wire-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i
class="wire-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
{#if !loading && !flatVisibleOptions().length}
<div class="wire-next__select-message">{emptyLabel}</div>
{/if}
</div>
{#if tags && query && !allOptions().some((option) => option.label.toLowerCase() === query.toLowerCase())}
<button type="button" class="wire-next__create-option">{createLabel} “{query}”</button>
{/if}
<button
type="button"
class="wire-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
<input
type="hidden"
name="{name}"
value="{multiple ? selectedValues.join(',') : selectedValue}"
required="{required}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
}
}
+154
View File
@@ -0,0 +1,154 @@
component AvatarGroup {
outputs {
overflow(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
items: unknown[] = []
size: string = "md"
color: string = "primary"
variant: string = "solid"
shape: string = "circle"
layout: string = "stack"
maxVisible: number = 4
columns: number = 3
borderColor: string = ""
showTooltips: boolean = true
overflowLabel: string = "Show remaining members"
class: string = ""
}
state overflowOpen: boolean = false
functions {
shared function visibleMembers() {
return items.slice(0, Number(maxVisible))
}
shared function hiddenMembers() {
return items.slice(Number(maxVisible))
}
client function toggleOverflow(sourceEvent, root, customEvent) {
overflowOpen = !overflowOpen
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("overflow", true, false, {
component: "AvatarGroup",
open: overflowOpen,
hiddenCount: hiddenMembers().length
})
root.dispatchEvent(customEvent)
}
}
view {
<div
{...attrs}
class="wire-next wire-next--avatar-group {class}"
data-wrn-avatar-group
data-layout="{layout}"
data-shape="{shape}"
data-size="{size}"
style="--wire-avatar-group-columns:{columns}; --wire-avatar-group-ring:{borderColor || 'var(--wire-color-bg)'}"
role="group"
aria-label="Avatar group"
>
<div
class="wire-next__avatar-group-members"
>
{#each visibleMembers() as item}
<span
class="wire-next__avatar-group-member"
data-size="{item.size || size}"
data-shape="{item.shape || shape}"
data-color="{item.color || color}"
data-variant="{item.variant || variant}"
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
>
<span
class="wire-next__avatar-group-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt="{item.alt || item.name || ''}"
loading="lazy"
/>
{:else if item.initials}
<span
aria-hidden="true"
>
{item.initials}
</span>
{:else}
<span
class="icon-[lucide--user-round]"
aria-hidden="true"
>
</span>
{/if}
</span>
{#if showTooltips && (item.tooltip || item.name)}
<span
class="wire-next__avatar-group-tooltip"
role="tooltip"
>
{item.tooltip || item.name}
</span>
{/if}
</span>
{/each}
{#if hiddenMembers().length > 0}
<span
class="wire-next__avatar-group-overflow"
>
<button
type="button"
class="wire-next__avatar-group-overflow-button"
aria-label="{overflowLabel}"
aria-expanded="{overflowOpen ? 'true' : 'false'}"
@click="toggleOverflow(event)"
>
+{hiddenMembers().length}
</button>
<span
class="wire-next__avatar-group-menu"
role="menu"
data-show="overflowOpen"
aria-hidden="{overflowOpen ? 'false' : 'true'}"
>
{#each hiddenMembers() as item}
<span
class="wire-next__avatar-group-menu-item"
role="menuitem"
>
<span
class="wire-next__avatar-group-menu-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt=""
loading="lazy"
/>
{:else}
<span>{item.initials || '?'}</span>
{/if}
</span>
<span>{item.name || item.alt || item.initials || 'Team member'}</span>
</span>
{/each}
</span>
</span>
{/if}
</div>
</div>
}
}
+67
View File
@@ -0,0 +1,67 @@
component Blockquote {
props {
quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."
citation: string = ""
citationTitle: string = ""
citationUrl: string = ""
avatarSrc: string = ""
avatarAlt: string = ""
size: string = "md"
color: string = "primary"
align: string = "left"
variant: string = "default"
quoteMark: boolean = true
italic: boolean = true
class: string = ""
}
view {
<figure
{...attrs}
class="wire-next wire-next--blockquote wire-next--color-{color} wire-next--size-{size} {class}"
data-size="{size}"
data-color="{color}"
data-align="{align}"
data-variant="{variant}"
data-italic="{italic}"
>
<blockquote cite="{citationUrl}">
{#if quoteMark}
<span class="wire-next__blockquote-mark" aria-hidden="true">“</span>
{/if}
<div class="wire-next__blockquote-copy">
{#if quote}
<p>{quote}</p>
{:else}
<slot />
{/if}
</div>
</blockquote>
{#if citation || citationTitle || avatarSrc}
<figcaption class="wire-next__blockquote-citation">
{#if avatarSrc}
<img
class="wire-next__blockquote-avatar"
src="{avatarSrc}"
alt="{avatarAlt}"
loading="lazy"
/>
{/if}
<span class="wire-next__blockquote-attribution">
{#if citation}
{#if citationUrl}
<cite><a href="{citationUrl}">{citation}</a></cite>
{:else}
<cite>{citation}</cite>
{/if}
{/if}
{#if citationTitle}<span>{citationTitle}</span>{/if}
</span>
</figcaption>
{/if}
</figure>
}
}
+323
View File
@@ -0,0 +1,323 @@
component Breadcrumb {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
label: string = "Breadcrumb"
items: unknown[] = []
active: string = ""
separator: string = "chevron"
showHome: boolean = false
homeLabel: string = "Home"
homeHref: string = "/"
homeIcon: string = "icon-[lucide--house]"
size: string = "default"
color: string = "primary"
variant: string = "minimal"
class: string = ""
}
view {
<nav
{...attrs}
data-ui-component="Breadcrumb"
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
aria-label='{label}'
class='wire-breadcrumb {class}'
>
<ol class="wire-breadcrumb__list">
{#if showHome}
<li class="wire-breadcrumb__item wire-breadcrumb__item--home">
<a
href='{homeHref || "/"}'
class="wire-breadcrumb__link wire-breadcrumb__home"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 } }))'
>
{#if homeIcon === "icon-[lucide--house]"}
<span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
{:else if homeIcon}
<span class='{homeIcon} wire-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__home-label">{homeLabel}</span>
</a>
</li>
{/if}
{#each items as item, itemIndex}
<li
class="wire-breadcrumb__item"
data-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "true" : "false"}'
>
{#if showHome || itemIndex > 0}
<span class="wire-breadcrumb__separator" aria-hidden="true">
{#if separator === "slash"}
<span>/</span>
{:else if separator === "dot"}
<span>•</span>
{:else if separator === "arrow"}
<span>→</span>
{:else}
<span class="icon-[lucide--chevron-right] wire-breadcrumb__separator-icon"></span>
{/if}
</span>
{/if}
{#if item.href && !item.disabled && !(item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1))}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
class="wire-breadcrumb__link"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
>
{#if item.icon}
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
{#if item.external}
<span class="icon-[lucide--arrow-up-right] wire-breadcrumb__external" aria-hidden="true"></span>
{/if}
</a>
{:else}
<span
class="wire-breadcrumb__current"
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : ""}'
aria-disabled='{item.disabled ? "true" : "false"}'
>
{#if item.icon}
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
</span>
{/if}
</li>
{/each}
</ol>
</nav>
}
style {
.wire-breadcrumb {
--breadcrumb-accent: var(--wire-color-primary);
--breadcrumb-soft: var(--wire-color-primary-soft);
--breadcrumb-muted: var(--wire-color-primary-muted);
--breadcrumb-text: var(--wire-color-primary-text);
min-width: 0;
color: var(--wire-color-text-muted);
}
.wire-breadcrumb[data-color="secondary"] {
--breadcrumb-accent: var(--wire-color-secondary);
--breadcrumb-soft: var(--wire-color-secondary-soft);
--breadcrumb-muted: var(--wire-color-secondary-muted);
--breadcrumb-text: var(--wire-color-secondary-text);
}
.wire-breadcrumb[data-color="info"] {
--breadcrumb-accent: var(--wire-color-info);
--breadcrumb-soft: var(--wire-color-info-soft);
--breadcrumb-muted: var(--wire-color-info-muted);
--breadcrumb-text: var(--wire-color-info-text);
}
.wire-breadcrumb[data-color="success"] {
--breadcrumb-accent: var(--wire-color-success);
--breadcrumb-soft: var(--wire-color-success-soft);
--breadcrumb-muted: var(--wire-color-success-muted);
--breadcrumb-text: var(--wire-color-success-text);
}
.wire-breadcrumb[data-color="warning"] {
--breadcrumb-accent: var(--wire-color-warning);
--breadcrumb-soft: var(--wire-color-warning-soft);
--breadcrumb-muted: var(--wire-color-warning-muted);
--breadcrumb-text: var(--wire-color-warning-text);
}
.wire-breadcrumb[data-color="danger"] {
--breadcrumb-accent: var(--wire-color-danger);
--breadcrumb-soft: var(--wire-color-danger-soft);
--breadcrumb-muted: var(--wire-color-danger-muted);
--breadcrumb-text: var(--wire-color-danger-text);
}
.wire-breadcrumb[data-variant="soft"] {
width: fit-content;
max-width: 100%;
padding: 0.45rem 0.65rem;
background: var(--breadcrumb-soft);
border: 1px solid var(--breadcrumb-muted);
border-radius: 0.75rem;
}
.wire-breadcrumb[data-variant="outline"] {
width: fit-content;
max-width: 100%;
padding: 0.45rem 0.65rem;
background: var(--wire-color-surface-raised);
border: 1px solid var(--wire-color-border);
border-radius: 0.75rem;
box-shadow: var(--wire-shadow-sm);
}
.wire-breadcrumb[data-variant="contrast"] {
color: rgba(255, 255, 255, 0.78);
}
.wire-breadcrumb__list {
display: flex;
align-items: center;
gap: 0;
min-width: 0;
margin: 0;
padding: 0;
overflow-x: auto;
list-style: none;
scrollbar-width: none;
white-space: nowrap;
}
.wire-breadcrumb__list::-webkit-scrollbar {
display: none;
}
.wire-breadcrumb__item {
display: inline-flex;
align-items: center;
min-width: 0;
flex: 0 0 auto;
}
.wire-breadcrumb__separator {
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline: 0.45rem;
color: var(--wire-color-text-subtle);
font-size: 0.75rem;
opacity: 0.72;
}
.wire-breadcrumb__separator-icon {
width: 0.9rem;
height: 0.9rem;
}
.wire-breadcrumb__link,
.wire-breadcrumb__current {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
min-height: 2rem;
padding: 0.25rem 0.4rem;
border-radius: 0.55rem;
font-size: 0.8125rem;
line-height: 1.25rem;
text-decoration: none;
}
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__link,
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__current {
min-height: 1.75rem;
font-size: 0.75rem;
}
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__link,
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__current {
min-height: 2.25rem;
font-size: 0.875rem;
}
.wire-breadcrumb__link {
color: var(--wire-color-text-muted);
font-weight: 600;
transition:
color 160ms ease,
background 160ms ease;
}
.wire-breadcrumb__link:hover {
color: var(--breadcrumb-accent);
background: var(--breadcrumb-soft);
}
.wire-breadcrumb__link:focus-visible {
color: var(--breadcrumb-accent);
outline: 2px solid var(--breadcrumb-accent);
outline-offset: 2px;
}
.wire-breadcrumb__current {
max-width: 22rem;
color: var(--wire-color-text);
font-weight: 750;
}
.wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
color: var(--breadcrumb-text);
background: var(--breadcrumb-soft);
}
.wire-breadcrumb__current[aria-disabled="true"] {
opacity: 0.58;
}
.wire-breadcrumb__icon,
.wire-breadcrumb__external {
flex: 0 0 auto;
width: 0.95rem;
height: 0.95rem;
}
.wire-breadcrumb__home {
color: var(--breadcrumb-accent);
}
.wire-breadcrumb__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link,
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__current,
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__home {
color: inherit;
}
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link:hover,
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
color: #ffffff;
background: rgba(255, 255, 255, 0.12);
}
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__separator {
color: rgba(255, 255, 255, 0.62);
}
@media (max-width: 639px) {
.wire-breadcrumb__home-label {
display: none;
}
.wire-breadcrumb__current {
max-width: 13rem;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-breadcrumb__link {
transition: none;
}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
component ButtonGroup {
outputs {
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
change(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
}
props {
items: unknown[] = []
value: string = ""
size: string = "md"
color: string = "primary"
variant: string = "default"
orientation: string = "horizontal"
responsive: boolean = false
attached: boolean = true
selectable: boolean = false
toolbar: boolean = false
disabled: boolean = false
ariaLabel: string = "Button group"
class: string = ""
}
state selectedValue = value
functions {
shared function itemValue(item, index) {
return item.value || item.label || String(index)
}
client function selectItem(item, index) {
previousValue = selectedValue
nextValue = itemValue(item, index)
output.select({
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
if (selectable && previousValue !== nextValue) {
selectedValue = nextValue
output.change({
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
}
}
}
view {
<div
{...attrs}
class="wire-next wire-next--button-group {class}"
role="{toolbar ? 'toolbar' : 'group'}"
aria-label="{ariaLabel}"
aria-orientation="{orientation}"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-orientation="{orientation}"
data-responsive="{responsive}"
data-attached="{attached}"
data-selectable="{selectable}"
data-disabled="{disabled}"
>
{#if items}
{#each items as item, index}
<button
type="{item.type || 'button'}"
class="wire-btn wire-btn--variant-{item.variant || variant} wire-btn--color-{item.color || color} wire-btn--size-{item.size || size}"
disabled="{disabled || item.disabled}"
aria-label="{item.ariaLabel || item.label}"
aria-pressed="{selectable ? (selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false') : ''}"
data-value="{item.value || item.label || String(index)}"
data-selected="{selectable && selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false'}"
@click="selectItem(item, index)"
>
{#if item.icon}
<span class="wire-btn__icon {item.icon}" aria-hidden="true"></span>
{/if}
<span class="wire-btn__label">{item.label}</span>
</button>
{/each}
{/if}
<slot />
</div>
}
}
+464
View File
@@ -0,0 +1,464 @@
// Interactive, hydration-safe content carousel.
component Carousel {
outputs {
initialize(payload: { index: number; count: number })
change(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })
previous(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
next(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
play(payload: { index: number; interval: number })
pause(payload: { index: number })
reachStart(payload: { index: number })
reachEnd(payload: { index: number })
dragStart(payload: { index: number; x: null })
dragEnd(payload: { index: number; distance: number })
}
props {
size: string = "default"
color: string = "primary"
title: string = ""
description: string = ""
items: unknown[] = []
activeIndex: number = 0
slidesPerView: number = 1
gap: string = "0.75rem"
showPagination: boolean = false
isAutoPlay: boolean = false
autoplayInterval: number = 4000
isInfiniteLoop: boolean = false
isRTL: boolean = false
isCentered: boolean = false
isDraggable: boolean = false
isAutoHeight: boolean = false
isSnap: boolean = false
showCounter: boolean = false
thumbnails: string = "none"
ariaLabel: string = "Content carousel"
variant: string = "default"
class: string = ""
}
state currentIndex = activeIndex
state playing: boolean = false
state dragOrigin = null
state dragOffset: number = 0
state autoplayTimer = null
state carouselRoot = null
functions {
shared function slideCount() {
return items.length
}
shared function maximumIndex(count, visible) {
count = slideCount()
visible = Math.max(1, Number(slidesPerView) || 1)
if (isCentered || isSnap) {
return Math.max(0, count - 1)
}
return Math.max(0, count - visible)
}
shared function normalizedIndex(index, maximum) {
maximum = maximumIndex()
if (slideCount() === 0) {
return 0
}
if (isInfiniteLoop) {
if (index < 0) {
return maximum
}
if (index > maximum) {
return 0
}
}
return Math.max(0, Math.min(index, maximum))
}
client function rememberRoot(sourceEvent, root) {
if (!sourceEvent || !sourceEvent.currentTarget) {
return
}
root = sourceEvent.currentTarget.closest(".wire-next--carousel")
if (root) {
carouselRoot = root
}
}
client function syncNavigation(sourceEvent, index, root, viewport, slides, slide, thumbnails, thumbnail, rail) {
rememberRoot(sourceEvent)
root = carouselRoot
if (!root) {
return
}
if (isSnap) {
viewport = root.querySelector(".wire-next__carousel-viewport")
slides = root.querySelectorAll(".wire-next__carousel-slide")
slide = slides[index]
if (viewport && slide) {
viewport.scrollTo({
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
behavior: "smooth"
})
}
}
thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button")
if (thumbnails[index]) {
thumbnail = thumbnails[index]
rail = thumbnail.parentElement
rail.scrollTo({
left: thumbnail.offsetLeft - (rail.clientWidth - thumbnail.clientWidth) / 2,
top: thumbnail.offsetTop - (rail.clientHeight - thumbnail.clientHeight) / 2,
behavior: "smooth"
})
}
}
client function handleSnapScroll(sourceEvent, slides, viewport, viewportCenter, closestIndex, closestDistance, slide, slideCenter, distance, previousIndex) {
if (!isSnap) {
return
}
rememberRoot(sourceEvent)
viewport = sourceEvent.currentTarget
slides = viewport.querySelectorAll(".wire-next__carousel-slide")
viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2
closestIndex = currentIndex
closestDistance = 1000000000
slides.forEach(function (candidate, index) {
slideCenter = candidate.offsetLeft + candidate.clientWidth / 2
distance = Math.abs(slideCenter - viewportCenter)
if (distance < closestDistance) {
closestDistance = distance
closestIndex = index
}
})
closestIndex = normalizedIndex(closestIndex)
if (closestIndex !== currentIndex) {
previousIndex = currentIndex
currentIndex = closestIndex
output.change({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex],
reason: "snap"
})
slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button")
slide = slides[currentIndex]
if (slide) {
viewport = slide.parentElement
viewport.scrollTo({
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
top: slide.offsetTop - (viewport.clientHeight - slide.clientHeight) / 2,
behavior: "smooth"
})
}
}
}
client function selectSlide(index, reason, sourceEvent, previousIndex) {
if (slideCount() === 0) {
return
}
previousIndex = currentIndex
currentIndex = normalizedIndex(Number(index))
syncNavigation(sourceEvent, currentIndex)
if (previousIndex === currentIndex && reason !== "initialize") {
return
}
output.change({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex],
reason: reason || "select"
})
if (currentIndex === 0) {
output.reachStart({ index: currentIndex })
}
if (currentIndex === maximumIndex()) {
output.reachEnd({ index: currentIndex })
}
}
client function previousSlide(sourceEvent, previousIndex) {
previousIndex = currentIndex
selectSlide(currentIndex - 1, "previous", sourceEvent)
output.previous({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex]
})
}
client function nextSlide(sourceEvent, previousIndex) {
previousIndex = currentIndex
selectSlide(currentIndex + 1, "next", sourceEvent)
output.next({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex]
})
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "ArrowLeft") {
sourceEvent.preventDefault()
if (isRTL) {
nextSlide(sourceEvent)
} else {
previousSlide(sourceEvent)
}
}
if (sourceEvent.key === "ArrowRight") {
sourceEvent.preventDefault()
if (isRTL) {
previousSlide(sourceEvent)
} else {
nextSlide(sourceEvent)
}
}
if (sourceEvent.key === "Home") {
sourceEvent.preventDefault()
selectSlide(0, "keyboard", sourceEvent)
}
if (sourceEvent.key === "End") {
sourceEvent.preventDefault()
selectSlide(maximumIndex(), "keyboard", sourceEvent)
}
}
client function beginDrag(sourceEvent) {
if (!isDraggable || isSnap) {
return
}
sourceEvent.preventDefault()
dragOrigin = sourceEvent.clientX
dragOffset = 0
sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId)
output.dragStart({ index: currentIndex, x: dragOrigin })
}
client function moveDrag(sourceEvent) {
if (!isDraggable || isSnap || dragOrigin === null) {
return
}
sourceEvent.preventDefault()
dragOffset = sourceEvent.clientX - dragOrigin
}
shared function cancelDrag() {
dragOrigin = null
dragOffset = 0
}
client function endDrag(sourceEvent, distance) {
if (!isDraggable || isSnap || dragOrigin === null) {
return
}
sourceEvent.preventDefault()
distance = dragOffset || sourceEvent.clientX - dragOrigin
dragOrigin = null
dragOffset = 0
if (Math.abs(distance) > 20) {
if ((distance < 0 && !isRTL) || (distance > 0 && isRTL)) {
nextSlide(sourceEvent)
} else {
previousSlide(sourceEvent)
}
}
output.dragEnd({
index: currentIndex,
distance: distance
})
}
shared function advanceAutoplay() {
if (currentIndex >= maximumIndex()) {
selectSlide(0, "autoplay")
} else {
selectSlide(currentIndex + 1, "autoplay")
}
}
client function startAutoplay() {
if (!isAutoPlay || playing || slideCount() < 2) {
return
}
playing = true
autoplayTimer = setInterval(advanceAutoplay, Math.max(1000, Number(autoplayInterval)))
output.play({ index: currentIndex, interval: autoplayInterval })
}
client function pauseAutoplay() {
if (autoplayTimer) {
clearInterval(autoplayTimer)
}
autoplayTimer = null
if (playing) {
output.pause({ index: currentIndex })
}
playing = false
}
}
lifecycle {
mount {
output.initialize({ index: currentIndex, count: slideCount() })
startAutoplay()
}
unmount {
if (autoplayTimer) {
clearInterval(autoplayTimer)
}
}
}
view {
<section
{...attrs}
class="wire-next wire-next--carousel wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}"
data-rtl="{isRTL}"
data-centered="{isCentered}"
data-draggable="{isDraggable && !isSnap}"
data-dragging="{dragOrigin !== null}"
data-auto-height="{isAutoHeight}"
data-snap="{isSnap}"
data-thumbnails="{thumbnails}"
dir="{isRTL ? 'rtl' : 'ltr'}"
role="region"
aria-roledescription="carousel"
aria-label="{ariaLabel}"
@keydown="handleKeydown(event)"
@mouseenter="rememberRoot(event); pauseAutoplay()"
@mouseleave="startAutoplay()"
@focusin="rememberRoot(event); pauseAutoplay()"
@focusout="startAutoplay()"
>
{#if title || description}
<header class="wire-next__carousel-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
<div class="wire-next__carousel-layout">
{#if thumbnails === "vertical"}
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}: {item.label || item.title}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'thumbnail', event)"
>
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
</button>
{/each}
</div>
{/if}
<div class="wire-next__carousel-main">
<div class="wire-next__carousel-stage">
<div
class="wire-next__carousel-viewport"
tabindex="0"
@scroll="handleSnapScroll(event)"
@pointerdown="beginDrag(event)"
@pointermove="moveDrag(event)"
@pointerup="endDrag(event)"
@pointercancel="cancelDrag()"
@lostpointercapture="cancelDrag()"
>
<div
class="wire-next__carousel-track"
style="--wire-carousel-index: {currentIndex}; --wire-carousel-per-view: {slidesPerView}; --wire-carousel-gap: {gap}; --wire-carousel-drag-offset: {dragOffset}px"
>
{#each items as item, index}
<article
class="wire-next__carousel-slide"
data-active="{index === currentIndex}"
role="group"
aria-roledescription="slide"
aria-label="{index + 1} of {items.length}"
aria-hidden="{index === currentIndex ? 'false' : 'true'}"
>
{#if item.imageSrc}
<img src="{item.imageSrc}" alt="{item.imageAlt || item.title || ''}" />
{/if}
<div class="wire-next__carousel-slide-content">
{#if item.eyebrow}<span>{item.eyebrow}</span>{/if}
{#if item.title || item.label}<h4>{item.title || item.label}</h4>{/if}
{#if item.description}<p>{item.description}</p>{/if}
{#if item.actionLabel}
<a href="{item.actionHref || '#'}">{item.actionLabel}</a>
{/if}
</div>
</article>
{/each}
<slot />
</div>
</div>
<button
class="wire-next__carousel-control wire-next__carousel-control--previous"
type="button"
aria-label="Previous slide"
disabled="{!isInfiniteLoop && currentIndex === 0}"
@click="previousSlide(event)"
>
<span class="icon-[lucide--chevron-left]" aria-hidden="true"></span>
</button>
<button
class="wire-next__carousel-control wire-next__carousel-control--next"
type="button"
aria-label="Next slide"
disabled="{!isInfiniteLoop && currentIndex === maximumIndex()}"
@click="nextSlide(event)"
>
<span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
</button>
{#if showCounter}
<output class="wire-next__carousel-counter" aria-live="polite">
{currentIndex + 1} / {items.length}
</output>
{/if}
</div>
{#if showPagination}
<div class="wire-next__carousel-pagination" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'pagination', event)"
></button>
{/each}
</div>
{/if}
{#if thumbnails === "horizontal"}
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}: {item.label || item.title}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'thumbnail', event)"
>
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
</button>
{/each}
</div>
{/if}
</div>
</div>
</section>
}
}
+25
View File
@@ -0,0 +1,25 @@
component Chart {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
dataPointClick(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
legendToggle(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
title: string = "Chart"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chart wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
+150
View File
@@ -0,0 +1,150 @@
component ChatBubble {
outputs {
action(payload: { action: boolean; item: string | number | boolean | null | object; index: number })
messageClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
avatarClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
linkClick(payload: { link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
}
props {
size: string = "default"
color: string = "primary"
title: string = ""
description: string = ""
items: unknown[] = []
oneSided: boolean = false
showAvatars: boolean = false
showMetadata: boolean = false
ariaLabel: string = "Conversation"
variant: string = "default"
class: string = ""
}
functions {
shared function messageDirection(item) {
return item.direction === "outgoing" ? "outgoing" : "incoming"
}
client function selectMessage(item, index) {
output.messageClick({
item: item,
index: index,
direction: messageDirection(item)
})
}
client function selectAvatar(sourceEvent, item, index) {
sourceEvent.stopPropagation()
output.avatarClick({
item: item,
index: index,
direction: messageDirection(item)
})
}
client function selectLink(sourceEvent, link, item, index) {
sourceEvent.stopPropagation()
output.linkClick({
link: link,
item: item,
index: index
})
}
client function selectAction(sourceEvent, item, index) {
sourceEvent.stopPropagation()
output.action({
action: item.action || "retry",
item: item,
index: index
})
}
}
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
data-one-sided="{oneSided}"
role="log"
aria-label="{ariaLabel}"
aria-live="polite"
>
{#if title || description}
<header class="wire-next__chat-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
{#if items.length}
<div class="wire-next__chat-thread">
{#each items as item, index}
<article
class="wire-next__chat-message"
data-direction="{messageDirection(item)}"
aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
>
<div class="wire-next__chat-row">
{#if showAvatars && (item.avatarSrc || item.avatarFallback)}
<button
class="wire-next__chat-avatar"
type="button"
aria-label="{item.avatarLabel || item.author || 'Message author'}"
@click="selectAvatar(event, item, index)"
>
{#if item.avatarSrc}
<img src="{item.avatarSrc}" alt="{item.avatarAlt || ''}" />
{:else}
<span>{item.avatarFallback}</span>
{/if}
</button>
{/if}
<div
class="wire-next__chat-content"
role="button"
tabindex="0"
@click="selectMessage(item, index)"
@keydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMessage(item, index) }"
>
{#if item.title}<h4>{item.title}</h4>{/if}
{#if item.text}<p>{item.text}</p>{/if}
{#if item.bullets && item.bullets.length}
<ul>
{#each item.bullets as bullet}<li>{bullet}</li>{/each}
</ul>
{/if}
{#if item.links && item.links.length}
<nav aria-label="Message links">
{#each item.links as link}
<a
href="{link.href || '#'}"
@click="selectLink(event, link, item, index)"
>{link.label}</a>
{/each}
</nav>
{/if}
</div>
</div>
{#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
<footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
{#if item.statusIcon}<span class="{item.statusIcon}" aria-hidden="true"></span>{/if}
{#if item.status}<span>{item.status}</span>{/if}
{#if item.timestamp}<time datetime="{item.datetime || ''}">{item.timestamp}</time>{/if}
{#if item.actionLabel}
<button type="button" @click="selectAction(event, item, index)">
{item.actionLabel}
</button>
{/if}
</footer>
{/if}
</article>
{/each}
</div>
{/if}
<slot />
</section>
}
}
+25
View File
@@ -0,0 +1,25 @@
component Clipboard {
outputs {
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Clipboard"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--clipboard wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
+98
View File
@@ -0,0 +1,98 @@
component Collapse {
outputs {
toggle(payload: { index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })
open(payload: { index: number; item: string | number | boolean | null | object })
close(payload: { index: number; item: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
items: unknown[] = []
multiple: boolean = false
mode: string = "panel"
initialOpenIndexes: unknown[] = []
ariaLabel: string = "Collapsible content"
class: string = ""
}
state openIndexes = initialOpenIndexes
functions {
shared function isOpen(index) {
return openIndexes.includes(index)
}
client function toggleItem(index, item, opening) {
if (item.disabled) {
return
}
opening = !isOpen(index)
if (opening) {
if (multiple) {
openIndexes = openIndexes.concat(index)
} else {
openIndexes = [index]
}
output.open({ index: index, item: item })
} else {
openIndexes = openIndexes.filter((openIndex) => openIndex !== index)
output.close({ index: index, item: item })
}
output.toggle({
index: index,
item: item,
open: opening,
openIndexes: openIndexes
})
}
}
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--collapse {class}"
data-mode="{mode}"
aria-label="{ariaLabel}"
>
{#each items as item, index}
<article class="wire-next__collapse-item" data-open="{isOpen(index)}">
{#if mode === "inline" && item.preview}
<p class="wire-next__collapse-preview">{item.preview}</p>
{/if}
<button
class="wire-next__collapse-trigger"
type="button"
aria-expanded="{isOpen(index) ? 'true' : 'false'}"
aria-controls="{item.id || 'collapse-panel-' + index}"
disabled="{item.disabled}"
@click="toggleItem(index, item)"
>
<span>{isOpen(index) ? (item.closeLabel || "Read less") : item.label}</span>
<span class="icon-[lucide--chevron-down] wire-next__collapse-chevron" aria-hidden="true"></span>
</button>
<div
id="{item.id || 'collapse-panel-' + index}"
class="wire-next__collapse-panel"
data-open="{isOpen(index)}"
aria-hidden="{isOpen(index) ? 'false' : 'true'}"
inert="{!isOpen(index)}"
>
<div class="wire-next__collapse-content">
{#if item.title}<h4>{item.title}</h4>{/if}
{#if item.content}<p>{item.content}</p>{/if}
{#if item.links && item.links.length}
<nav aria-label="Related links">
{#each item.links as link}<a href="{link.href || '#'}">{link.label}</a>{/each}
</nav>
{/if}
</div>
</div>
</article>
{/each}
<slot />
</section>
}
}
+41
View File
@@ -0,0 +1,41 @@
component ColorPicker {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Color"
hiddenLabel: boolean = false
placeholder: string = ""
value: string = "#2563eb"
icon: string = ""
iconPosition: string = "start"
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
functions {
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--color-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__color-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<input id="{id || name}" type="color" name="{name}" value="{value}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" /><output>{value}</output></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
+35
View File
@@ -0,0 +1,35 @@
component Columns {
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
class: string = ""
}
view {
<div
data-ui-component="Columns"
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 items-start {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:md:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:gap-3='gap === "sm"'
class:gap-6='gap === "md"'
class:gap-10='gap === "lg"'
class:gap-14='gap === "xl"'
>
<slot></slot>
</div>
}
}
+389
View File
@@ -0,0 +1,389 @@
component ComboBox {
outputs {
search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "ComboBox"
name: string = ""
value: string = ""
options: unknown[] = []
groups: unknown[] = []
placeholder: string = "Search or select an option"
searchPlaceholder: string = "Start typing…"
clearable: boolean = true
allowCustomValue: boolean = false
disabled: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
helpText: string = ""
loading: boolean = false
loadingLabel: string = "Loading suggestions…"
emptyLabel: string = "No matching options"
clearLabel: string = "Clear value"
toggleLabel: string = "Toggle suggestions"
searchMode: string = "contains"
searchFields: string = "label,description"
minSearchLength: number = 0
searchResultLimit: number = 0
optionTemplate: string = "default"
defaultOpen: boolean = false
closeOnSelect: boolean = true
fixed: boolean = false
placement: string = "bottom"
autocomplete: string = "off"
remote: boolean = false
remoteUrl: string = ""
remoteQueryParam: string = "q"
remoteDebounce: number = 250
remoteAutoLoad: boolean = true
infinite: boolean = false
hasMore: boolean = false
page: number = 1
loadMoreLabel: string = "Load more"
class: string = ""
}
state open = defaultOpen
state query: string = ""
state selectedValue = value
state activeIndex: number = -1
functions {
shared function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
shared function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
shared function matches(option) {
if (!query || query.length < minSearchLength) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
shared function matchingOptions(list) {
return list.filter((option) => matches(option))
}
shared function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, searchResultLimit)
}
return matchingOptions(list)
}
shared function flatVisibleOptions() {
return visibleOptions(allOptions())
}
shared function isSelected(option) {
return selectedValue === option.value
}
shared function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
shared function selectedText() {
return selectedOptions().length ? selectedOptions()[0].label : selectedValue
}
shared function inputText() {
return query || selectedText()
}
shared function shouldShowDropdown() {
if (!open) {
return false
}
if (loading) {
return true
}
if (remote && query.length !== 0 && query.length < minSearchLength) {
return true
}
if (flatVisibleOptions().length) {
return true
}
return infinite && hasMore
}
shared function canSelect(option) {
return !option.disabled
}
shared function chooseOption(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = option.label
activeIndex = optionIndex(option)
if (closeOnSelect) {
open = false
}
}
client function updateQuery(event) {
query = event.target.value
selectedValue = allowCustomValue ? event.target.value : ""
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
client function clearValue(event) {
if (event) {
event.stopPropagation()
}
query = ""
selectedValue = ""
activeIndex = -1
open = false
}
shared function openDropdown() {
if (!disabled) {
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
}
shared function closeDropdown() {
open = false
}
shared function toggle() {
if (open) {
closeDropdown()
} else {
openDropdown()
}
}
shared function setValue(nextValue) {
selectedValue = nextValue
query = ""
}
shared function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
client function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
openDropdown()
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
openDropdown()
moveActive(-1)
} else if (event.key === "Enter" && open && activeIndex >= 0) {
event.preventDefault()
chooseOption(flatVisibleOptions()[activeIndex])
} else if (event.key === "Escape") {
closeDropdown()
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
shared function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-select wire-next--combobox {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
data-wrn-combobox
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { closeDropdown() }"
>
<label id="{name}-label" for="{name}-input">{label}</label>
<div class="wire-next__select-control wire-next__combobox-control">
<span class="wire-next__search-icon icon-[lucide--search]" aria-hidden="true"></span>
<input
{...attrs}
id="{name}-input"
class="wire-next__select-trigger wire-next__combobox-input"
type="text"
role="combobox"
name="{allowCustomValue ? name : ''}"
value="{inputText()}"
placeholder="{placeholder}"
autocomplete="{autocomplete}"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
required="{required && allowCustomValue}"
@focus="openDropdown()"
@click="openDropdown()"
@input="updateQuery(event)"
@keydown="handleKeydown(event)"
/>
<button
type="button"
class="wire-next__clear-select"
data-show="clearable && inputText() && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearValue(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
<button
type="button"
class="wire-next__combobox-toggle"
aria-label="{toggleLabel}"
tabindex="-1"
disabled="{disabled}"
@click="toggle()"
>
<span class="wire-next__select-chevron icon-[lucide--chevrons-up-down]" aria-hidden="true"></span>
</button>
</div>
<div
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
data-show="shouldShowDropdown()"
style="{shouldShowDropdown() ? '' : 'display: none'}"
>
<div
class="wire-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<div class="wire-next__select-message" data-show="loading" role="status">
<i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
role="listbox"
>
{#each groups as group}
{#if visibleOptions(group.options || []).length}
<div class="wire-next__option-group" role="group" aria-label="{group.label}">
<div class="wire-next__group-label">{group.label}</div>
{#each group.options || [] as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
<button
type="button"
class="wire-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
{#if !allowCustomValue}
<input
type="hidden"
name="{name}"
value="{selectedValue}"
required="{required}"
data-error="{name}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{/if}
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
}
}
+24
View File
@@ -0,0 +1,24 @@
component Confetti {
outputs {
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Confetti"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--confetti wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
+521
View File
@@ -0,0 +1,521 @@
component ContextMenu {
outputs {
open(payload: { x: number; y: number; trigger: string; sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
}
props {
items: unknown[] = []
open: boolean = false
defaultOpen: boolean = false
trigger: string = "contextmenu"
placement: string = "pointer"
align: string = "start"
size: string = "default"
color: string = "primary"
variant: string = "raised"
title: string = ""
description: string = ""
label: string = "Context menu"
closeOnSelect: boolean = true
closeOnOutside: boolean = true
disabled: boolean = false
minWidth: string = "14rem"
maxWidth: string = "20rem"
class: string = ""
}
state visible = defaultOpen
state positionX: number = 16
state positionY: number = 16
functions {
shared function isOpen() {
return open || visible
}
client function showMenu(sourceEvent) {
if (disabled) {
return
}
if (sourceEvent && sourceEvent.type === "contextmenu") {
sourceEvent.preventDefault()
}
if (placement === "pointer" && sourceEvent) {
positionX = Math.max(12, Math.min(sourceEvent.clientX || 12, window.innerWidth - 340))
positionY = Math.max(12, Math.min(sourceEvent.clientY || 12, window.innerHeight - 420))
}
visible = true
output.open({
x: positionX,
y: positionY,
trigger: trigger,
sourceEvent: sourceEvent
})
}
client function hideMenu(reason, sourceEvent) {
visible = false
output.close({
reason: reason,
sourceEvent: sourceEvent
})
}
client function toggleMenu(sourceEvent) {
if (isOpen()) {
hideMenu("toggle", sourceEvent)
} else {
showMenu(sourceEvent)
}
}
client function chooseItem(item, itemIndex, sourceEvent) {
if (disabled || item.disabled || item.type === "header" || item.type === "divider") {
sourceEvent.preventDefault()
return
}
output.select({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
if (item.action) {
output.action({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
}
if (closeOnSelect) {
hideMenu("select", sourceEvent)
}
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wire-context-menu") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
if (!options.length) {
return
}
const activeIndex = options.indexOf(document.activeElement)
const nextIndex = (activeIndex + direction + options.length) % options.length
options[nextIndex].focus()
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hideMenu("escape", sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
moveFocus(sourceEvent, 1)
} else if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
moveFocus(sourceEvent, -1)
}
}
}
view {
<div
{...attrs}
data-ui-component="ContextMenu"
data-open='{open || visible ? "true" : "false"}'
data-trigger='{trigger}'
data-placement='{placement}'
data-align='{align}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wire-context-menu {class}'
style='--wire-context-x:{positionX}px;--wire-context-y:{positionY}px;--wire-context-min-width:{minWidth};--wire-context-max-width:{maxWidth};'
@keydown='handleKeydown(event)'
>
<div
class="wire-context-menu__trigger"
tabindex='{disabled ? "-1" : "0"}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@contextmenu='if (trigger === "contextmenu" || trigger === "both") { showMenu(event) }'
@click='if (trigger === "click" || trigger === "both") { toggleMenu(event) }'
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggleMenu(event) }'
>
<slot name="trigger"></slot>
</div>
{#if closeOnOutside}
<button
type="button"
class="wire-context-menu__dismiss-layer"
data-show='{open || visible}'
aria-label="Close context menu"
@click='hideMenu("outside", event)'
></button>
{/if}
<div
class="wire-context-menu__panel"
data-show='{open || visible}'
role="menu"
aria-label='{label}'
>
{#if title || description}
<div class="wire-context-menu__header">
{#if title}
<strong>{title}</strong>
{/if}
{#if description}
<span>{description}</span>
{/if}
</div>
{/if}
<slot name="header"></slot>
<div class="wire-context-menu__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wire-context-menu__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wire-context-menu__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
role="menuitem"
data-context-menu-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
class="wire-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
{:else if item.external}
<span class="icon-[lucide--arrow-up-right] wire-context-menu__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
role="menuitem"
data-context-menu-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wire-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
{/each}
</div>
<slot></slot>
<slot name="footer"></slot>
</div>
</div>
}
style {
.wire-context-menu {
--context-accent: var(--wire-color-primary);
--context-soft: var(--wire-color-primary-soft);
position: relative;
display: block;
min-width: 0;
}
.wire-context-menu[data-color="secondary"] {
--context-accent: var(--wire-color-secondary);
--context-soft: var(--wire-color-secondary-soft);
}
.wire-context-menu[data-color="info"] {
--context-accent: var(--wire-color-info);
--context-soft: var(--wire-color-info-soft);
}
.wire-context-menu[data-color="success"] {
--context-accent: var(--wire-color-success);
--context-soft: var(--wire-color-success-soft);
}
.wire-context-menu[data-color="warning"] {
--context-accent: var(--wire-color-warning);
--context-soft: var(--wire-color-warning-soft);
}
.wire-context-menu[data-color="danger"] {
--context-accent: var(--wire-color-danger);
--context-soft: var(--wire-color-danger-soft);
}
.wire-context-menu__trigger {
display: block;
min-width: 0;
outline: none;
}
.wire-context-menu__trigger:focus-visible {
border-radius: 0.6rem;
outline: 2px solid var(--wire-color-focus);
outline-offset: 3px;
}
.wire-context-menu__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1090;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wire-context-menu__panel {
position: absolute;
z-index: 1091;
top: calc(100% + 0.5rem);
left: 0;
width: max-content;
min-width: var(--wire-context-min-width);
max-width: min(var(--wire-context-max-width), calc(100vw - 1.5rem));
padding: 0.45rem;
color: var(--wire-color-text);
background:
linear-gradient(145deg, color-mix(in srgb, var(--context-accent) 5%, transparent), transparent 58%),
color-mix(in srgb, var(--wire-color-surface-raised) 96%, transparent);
border: 1px solid color-mix(in srgb, var(--context-accent) 18%, var(--wire-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 24px 64px color-mix(in srgb, black 24%, transparent);
backdrop-filter: blur(18px);
transform-origin: top left;
}
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
position: fixed;
top: var(--wire-context-y);
left: var(--wire-context-x);
}
.wire-context-menu[data-placement="bottom-end"] .wire-context-menu__panel {
right: 0;
left: auto;
transform-origin: top right;
}
.wire-context-menu[data-placement="top-start"] .wire-context-menu__panel {
top: auto;
bottom: calc(100% + 0.5rem);
transform-origin: bottom left;
}
.wire-context-menu[data-placement="top-end"] .wire-context-menu__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.5rem);
left: auto;
transform-origin: bottom right;
}
.wire-context-menu[data-variant="soft"] .wire-context-menu__panel {
background:
linear-gradient(145deg, var(--context-soft), transparent 72%),
var(--wire-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wire-context-menu[data-variant="outline"] .wire-context-menu__panel {
background: var(--wire-color-surface-raised);
box-shadow: none;
}
.wire-context-menu__header {
display: grid;
gap: 0.2rem;
padding: 0.65rem 0.75rem 0.75rem;
border-bottom: 1px solid var(--wire-color-border);
}
.wire-context-menu__header strong {
font-size: 0.86rem;
font-weight: 650;
}
.wire-context-menu__header span {
color: var(--wire-color-text-muted);
font-size: 0.75rem;
line-height: 1.45;
}
.wire-context-menu__items {
display: grid;
gap: 0.15rem;
padding-block: 0.25rem;
}
.wire-context-menu__section-label {
padding: 0.55rem 0.75rem 0.3rem;
color: var(--wire-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wire-context-menu__divider {
height: 1px;
margin: 0.3rem 0.45rem;
background: var(--wire-color-border);
}
.wire-context-menu__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.7rem;
width: 100%;
min-width: 0;
padding: 0.65rem 0.7rem;
color: var(--wire-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
}
.wire-context-menu__item:hover,
.wire-context-menu__item:focus-visible,
.wire-context-menu__item[data-selected="true"] {
color: var(--context-accent);
background: var(--context-soft);
outline: none;
}
.wire-context-menu__item:active {
transform: scale(0.985);
}
.wire-context-menu__item[data-danger="true"] {
color: var(--wire-color-danger);
}
.wire-context-menu__item[data-danger="true"]:hover,
.wire-context-menu__item[data-danger="true"]:focus-visible {
background: var(--wire-color-danger-soft);
}
.wire-context-menu__item[disabled],
.wire-context-menu__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wire-context-menu__icon,
.wire-context-menu__status {
width: 1rem;
height: 1rem;
color: currentColor;
}
.wire-context-menu__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wire-context-menu__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-context-menu__copy small {
overflow: hidden;
color: var(--wire-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-context-menu kbd {
padding: 0.16rem 0.38rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.36rem;
font-size: 0.64rem;
font-family: inherit;
}
.wire-context-menu[data-size="sm"] .wire-context-menu__item {
padding: 0.52rem 0.6rem;
}
.wire-context-menu[data-size="lg"] .wire-context-menu__item {
padding: 0.78rem 0.82rem;
}
@media (max-width: 639px) {
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
max-width: none;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-context-menu__item {
transition: none;
}
}
}
}
+26
View File
@@ -0,0 +1,26 @@
component CopyMarkup {
outputs {
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Copy Markup"
name: string = ""
value: string = ""
placeholder: string = ""
type: string = "text"
min: string = ""
max: string = ""
step: string = ""
disabled: boolean = false
required: boolean = false
class: string = ""
}
view {
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--copy-markup {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
}
}
@@ -0,0 +1,17 @@
component CustomScrollbar {
outputs {
scroll(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
class: string = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--custom-scrollbar wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
}
}
+24
View File
@@ -0,0 +1,24 @@
component DataMap {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Data Map"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--data-map wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
+22
View File
@@ -0,0 +1,22 @@
component DataTable {
outputs {
sort(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
rowClick(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
pageChange(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
caption: string = "Data Table"
columns: unknown[] = []
rows: unknown[] = []
striped: boolean = true
class: string = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--table {class}"><table><caption>{caption}</caption><thead><tr>{#each columns as column}<th>{column.label}</th>{/each}</tr></thead><tbody>{#each rows as row}<tr>{#each columns as column}<td>{row[column.key]}</td>{/each}</tr>{/each}</tbody></table><slot /></div>
}
}
+62
View File
@@ -0,0 +1,62 @@
component DatePicker {
outputs {
input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
open(payload: { value: string; name: string; sourceEvent: Event })
close(payload: { value: string; name: string; sourceEvent: Event })
focus(payload: { value: string; name: string; sourceEvent: Event })
blur(payload: { value: string; name: string; sourceEvent: Event })
invalid(payload: { name: string; message: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Date Picker"
id: string = ""
name: string = ""
value: string = ""
placeholder: string = ""
type: string = "date"
locale: string = "en-US"
firstDayOfWeek: number = 0
months = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
years = [2024, 2025, 2026, 2027, 2028, 2029, 2030]
min: string = ""
max: string = ""
step: string = ""
helperText: string = ""
cornerHint: string = ""
error: string = ""
variant: string = "normal"
inline: boolean = false
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state currentValue = value
state selectedYear = value ? value.split("-")[0] : ""
state selectedMonth = value ? value.split("-")[1] : ""
state selectedDay = value ? value.split("-")[2] : ""
state expanded: boolean = false
functions {
client function openCalendar(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function updateDate(part, nextValue, sourceEvent, nextDate, detail) { if (part === "year") { selectedYear = String(nextValue) } if (part === "month") { selectedMonth = String(nextValue).padStart(2, "0") } if (part === "day") { selectedDay = String(nextValue).padStart(2, "0") } if (!selectedYear || !selectedMonth || !selectedDay) { return } nextDate = selectedYear + "-" + selectedMonth + "-" + selectedDay; if ((min && nextDate < min) || (max && nextDate > max)) { return } currentValue = nextDate; detail = { value: currentValue, year: selectedYear, month: selectedMonth, day: selectedDay, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function finishDate(sourceEvent) { if (!currentValue) { return } expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--date-picker"); input = root.querySelector(".wire-next__picker-value"); nextDate = input.getAttribute("value").slice(0, 5) + root.querySelector("select").getAttribute("value") + "-" + sourceEvent.currentTarget.dataset.value; currentValue = nextDate; input.setAttribute("value", nextDate); trigger = root.querySelector(".wire-next__date-trigger span"); if (trigger) { trigger.replaceChildren(nextDate) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: nextDate, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function clearDate(sourceEvent, detail) { if (disabled || readonly) { return } currentValue = ""; detail = { value: currentValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
<div class="wire-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__picker-control"><input id="{id || name}" class="wire-next__sr-only wire-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wire-next__date-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select date"}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wire-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
<div class="wire-next__calendar" role="dialog" aria-label="{label}"><div class="wire-next__date-selectors"><label>Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label>Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)">{#each years as year}<option value="{year}" selected="{String(year) === selectedYear}">{year}</option>{/each}</select></label></div><div class="wire-next__calendar-grid">{#each days as day}<button type="button" data-value="{String(day).padStart(2, '0')}" aria-pressed="{String(day).padStart(2, '0') === selectedDay}" @click="selectDay(event)">{day}</button>{/each}</div><button type="button" class="wire-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
+34
View File
@@ -0,0 +1,34 @@
component DeviceFrame {
outputs {
change(payload: { device: string; orientation: string; sourceEvent: Event })
rotate(payload: { device: string; orientation: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Device Frame"
description: string = ""
items: unknown[] = []
variant: string = "default"
device: string = "phone"
orientation: string = "portrait"
src: string = ""
srcdoc: string = ""
frameTitle: string = "Device preview"
showToolbar: boolean = true
allow: string = ""
class: string = ""
}
state currentOrientation = orientation
functions {
client function setDevice(nextDevice, sourceEvent) { output.change({ device: nextDevice, orientation: currentOrientation, sourceEvent: sourceEvent }) }
client function rotateFrame(sourceEvent) { currentOrientation = currentOrientation === "portrait" ? "landscape" : "portrait"; output.rotate({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }); output.change({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }) }
}
view {
<section {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--device-frame wire-next--variant-{variant} {class}" data-device="{device}" data-orientation="{currentOrientation}">
<header><div>{#if title}<strong>{title}</strong>{/if}{#if description}<p>{description}</p>{/if}</div>{#if showToolbar}<div class="wire-next__device-actions" role="toolbar" aria-label="Preview device">{#each items as item}<button type="button" aria-pressed="{item.value === device}" @click="setDevice(item.value, event)">{item.label}</button>{/each}<button type="button" aria-label="Rotate preview" @click="rotateFrame(event)">↻</button></div>{/if}</header>
<div class="wire-next__device-shell">{#if src || srcdoc}<iframe title="{frameTitle}" src="{src}" srcdoc="{srcdoc}" allow="{allow}"></iframe>{:else}<div class="wire-next__device-content"><slot /></div>{/if}</div>
</section>
}
}
+28
View File
@@ -0,0 +1,28 @@
component DragAndDrop {
outputs {
dragStart(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragEnd(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragEnter(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragLeave(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
drop(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Drag And Drop"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--drag-and-drop wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
+453
View File
@@ -0,0 +1,453 @@
component Drawer {
outputs {
open(payload: { placement: string; sourceEvent: Event })
close(payload: { reason: string; placement: string; sourceEvent: Event })
cancel(payload: { placement: string; sourceEvent: Event })
}
props {
open: boolean = false
defaultOpen: boolean = false
placement: string = "right"
size: string = "md"
color: string = "primary"
variant: string = "default"
title: string = "Drawer"
description: string = ""
icon: string = ""
label: string = "Drawer"
closeLabel: string = "Close drawer"
showClose: boolean = true
closeOnBackdrop: boolean = true
closeOnEscape: boolean = true
overlay: boolean = true
scrollable: boolean = true
triggerLabel: string = ""
triggerIcon: string = ""
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showDrawer(sourceEvent) {
visible = true
output.open({
placement: placement,
sourceEvent: sourceEvent
})
}
client function hideDrawer(reason, sourceEvent) {
visible = false
output.close({
reason: reason,
placement: placement,
sourceEvent: sourceEvent
})
}
client function cancelDrawer(sourceEvent) {
output.cancel({
placement: placement,
sourceEvent: sourceEvent
})
hideDrawer("cancel", sourceEvent)
}
client function handleKeydown(sourceEvent) {
if (closeOnEscape && sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
cancelDrawer(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-ui-component="Drawer"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-overlay='{overlay ? "true" : "false"}'
data-scrollable='{scrollable ? "true" : "false"}'
class='wire-drawer {class}'
>
{#if triggerLabel}
<button
type="button"
class="wire-drawer__trigger"
aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}'
@click='showDrawer(event)'
>
{#if triggerIcon}
<span class='{triggerIcon}' aria-hidden="true"></span>
{/if}
<span>{triggerLabel}</span>
</button>
{/if}
<span class="wire-drawer__trigger-slot" @click='showDrawer(event)'>
<slot name="trigger"></slot>
</span>
<div
class="wire-drawer__layer"
data-show='{open || visible}'
role="presentation"
@keydown='handleKeydown(event)'
>
{#if overlay}
<button
type="button"
class="wire-drawer__backdrop"
aria-label='{closeLabel}'
@click='if (closeOnBackdrop) { cancelDrawer(event) }'
></button>
{/if}
<section
class="wire-drawer__panel"
role="dialog"
aria-modal="true"
aria-label='{label || title}'
tabindex="-1"
>
<div class="wire-drawer__handle" aria-hidden="true"></div>
<header class="wire-drawer__header">
<div class="wire-drawer__heading">
{#if icon}
<span class='wire-drawer__icon {icon}' aria-hidden="true"></span>
{/if}
<div class="wire-drawer__heading-copy">
<slot name="header"></slot>
{#if title}
<h2>{title}</h2>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
</div>
{#if showClose}
<button
type="button"
class="wire-drawer__close"
aria-label='{closeLabel}'
@click='hideDrawer("close-button", event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</header>
<div class="wire-drawer__body">
<slot></slot>
</div>
<footer class="wire-drawer__footer">
<slot name="footer"></slot>
</footer>
</section>
</div>
</div>
}
style {
.wire-drawer {
--drawer-accent: var(--wire-color-primary);
--drawer-soft: var(--wire-color-primary-soft);
position: relative;
display: inline-flex;
}
.wire-drawer[data-color="secondary"] {
--drawer-accent: var(--wire-color-secondary);
--drawer-soft: var(--wire-color-secondary-soft);
}
.wire-drawer[data-color="info"] {
--drawer-accent: var(--wire-color-info);
--drawer-soft: var(--wire-color-info-soft);
}
.wire-drawer[data-color="success"] {
--drawer-accent: var(--wire-color-success);
--drawer-soft: var(--wire-color-success-soft);
}
.wire-drawer[data-color="warning"] {
--drawer-accent: var(--wire-color-warning);
--drawer-soft: var(--wire-color-warning-soft);
}
.wire-drawer[data-color="danger"] {
--drawer-accent: var(--wire-color-danger);
--drawer-soft: var(--wire-color-danger-soft);
}
.wire-drawer__trigger,
.wire-drawer__trigger-slot {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.wire-drawer__trigger {
appearance: none;
min-height: 2.55rem;
padding: 0.65rem 1rem;
color: var(--wire-color-primary-contrast);
background: var(--drawer-accent);
border: 0;
border-radius: 0.8rem;
font: inherit;
font-size: 0.85rem;
font-weight: 650;
cursor: pointer;
}
.wire-drawer__layer {
position: fixed;
inset: 0;
z-index: 1200;
display: flex;
pointer-events: none;
}
.wire-drawer__backdrop {
position: absolute;
inset: 0;
z-index: 0;
appearance: none;
padding: 0;
background: color-mix(in srgb, black 56%, transparent);
border: 0;
backdrop-filter: blur(7px);
pointer-events: auto;
}
.wire-drawer[data-overlay="false"] .wire-drawer__backdrop {
display: none;
}
.wire-drawer__panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(28rem, calc(100vw - 1rem));
max-height: 100%;
margin-left: auto;
color: var(--wire-color-text);
background:
radial-gradient(
circle at 100% 0%,
color-mix(in srgb, var(--drawer-accent) 8%, transparent),
transparent 34%
),
var(--wire-color-surface-raised);
border-left: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
pointer-events: auto;
overflow: hidden;
}
.wire-drawer[data-size="sm"] .wire-drawer__panel {
width: min(22rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="lg"] .wire-drawer__panel {
width: min(38rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="xl"] .wire-drawer__panel {
width: min(52rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="full"] .wire-drawer__panel {
width: 100vw;
}
.wire-drawer[data-placement="left"] .wire-drawer__panel {
margin-right: auto;
margin-left: 0;
border-right: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
border-left: 0;
box-shadow: 28px 0 80px color-mix(in srgb, black 28%, transparent);
}
.wire-drawer[data-placement="top"] .wire-drawer__layer,
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
align-items: flex-start;
}
.wire-drawer[data-placement="top"] .wire-drawer__panel,
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
width: 100%;
max-height: min(80vh, 44rem);
margin: 0;
border: 0;
box-shadow: 0 24px 80px color-mix(in srgb, black 28%, transparent);
}
.wire-drawer[data-placement="top"] .wire-drawer__panel {
border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
}
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
align-items: flex-end;
}
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
border-top: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
border-radius: 1.3rem 1.3rem 0 0;
}
.wire-drawer[data-variant="soft"] .wire-drawer__panel {
background:
linear-gradient(145deg, var(--drawer-soft), transparent 65%),
var(--wire-color-surface-raised);
}
.wire-drawer[data-variant="solid"] .wire-drawer__panel {
color: var(--wire-color-primary-contrast);
background: var(--drawer-accent);
border-color: color-mix(in srgb, white 18%, transparent);
}
.wire-drawer__handle {
display: none;
width: 2.75rem;
height: 0.28rem;
margin: 0.55rem auto 0;
background: color-mix(in srgb, var(--wire-color-text-muted) 48%, transparent);
border-radius: 999px;
}
.wire-drawer[data-placement="bottom"] .wire-drawer__handle {
display: block;
}
.wire-drawer__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.35rem 1.35rem 1.1rem;
border-bottom: 1px solid var(--wire-color-border);
}
.wire-drawer__heading {
display: flex;
align-items: flex-start;
gap: 0.85rem;
min-width: 0;
}
.wire-drawer__icon {
flex: 0 0 auto;
width: 1.25rem;
height: 1.25rem;
margin-top: 0.15rem;
color: var(--drawer-accent);
}
.wire-drawer[data-variant="solid"] .wire-drawer__icon {
color: currentColor;
}
.wire-drawer__heading-copy {
display: grid;
gap: 0.3rem;
min-width: 0;
}
.wire-drawer__heading-copy h2,
.wire-drawer__heading-copy p {
margin: 0;
}
.wire-drawer__heading-copy h2 {
font-size: 1.05rem;
font-weight: 650;
line-height: 1.3;
}
.wire-drawer__heading-copy p {
color: var(--wire-color-text-muted);
font-size: 0.8rem;
line-height: 1.55;
}
.wire-drawer[data-variant="solid"] .wire-drawer__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
.wire-drawer__close {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 2.35rem;
height: 2.35rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.75rem;
cursor: pointer;
}
.wire-drawer__close:hover,
.wire-drawer__close:focus-visible {
color: var(--drawer-accent);
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wire-color-border));
outline: none;
}
.wire-drawer__body {
flex: 1 1 auto;
min-height: 0;
padding: 1.35rem;
}
.wire-drawer[data-scrollable="true"] .wire-drawer__body {
overflow: auto;
overscroll-behavior: contain;
}
.wire-drawer__footer {
padding: 1rem 1.35rem 1.35rem;
border-top: 1px solid var(--wire-color-border);
}
.wire-drawer__footer:empty {
display: none;
}
@media (max-width: 639px) {
.wire-drawer[data-placement="right"] .wire-drawer__panel,
.wire-drawer[data-placement="left"] .wire-drawer__panel {
width: min(24rem, 100vw);
}
.wire-drawer__header,
.wire-drawer__body,
.wire-drawer__footer {
padding-inline: 1rem;
}
}
}
}
+572
View File
@@ -0,0 +1,572 @@
component Dropdown {
outputs {
toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
open(payload: { sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
}
props {
items: unknown[] = []
open: boolean = false
defaultOpen: boolean = false
label: string = "Open menu"
icon: string = ""
showChevron: boolean = true
menuLabel: string = "Dropdown menu"
placement: string = "bottom-start"
width: string = "md"
size: string = "default"
color: string = "primary"
variant: string = "raised"
closeOnSelect: boolean = true
closeOnOutside: boolean = true
disabled: boolean = false
emptyLabel: string = "No menu items"
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showMenu(sourceEvent) {
if (disabled) {
return
}
visible = true
output.open({ sourceEvent: sourceEvent })
output.toggle({ open: true, sourceEvent: sourceEvent })
}
client function hideMenu(reason, sourceEvent) {
visible = false
output.close({ reason: reason, sourceEvent: sourceEvent })
output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
}
client function toggleMenu(sourceEvent) {
if (isOpen()) {
hideMenu("toggle", sourceEvent)
} else {
showMenu(sourceEvent)
}
}
client function chooseItem(item, itemIndex, sourceEvent) {
if (disabled || item.disabled || item.type === "divider" || item.type === "header") {
sourceEvent.preventDefault()
return
}
output.select({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
if (item.action) {
output.action({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
}
if (closeOnSelect) {
hideMenu("select", sourceEvent)
}
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (!options.length) {
return
}
const activeIndex = options.indexOf(document.activeElement)
const nextIndex = (activeIndex + direction + options.length) % options.length
options[nextIndex].focus()
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hideMenu("escape", sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
if (!isOpen()) {
showMenu(sourceEvent)
}
moveFocus(sourceEvent, 1)
} else if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
if (!isOpen()) {
showMenu(sourceEvent)
}
moveFocus(sourceEvent, -1)
} else if (sourceEvent.key === "Home" && isOpen()) {
sourceEvent.preventDefault()
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const first = root.querySelector("[data-dropdown-item]:not([disabled])")
if (first) {
first.focus()
}
} else if (sourceEvent.key === "End" && isOpen()) {
sourceEvent.preventDefault()
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (options.length) {
options[options.length - 1].focus()
}
}
}
}
view {
<div
{...attrs}
data-ui-component="Dropdown"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-width='{width}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wire-dropdown {class}'
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wire-dropdown__trigger"
disabled='{disabled}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@click='toggleMenu(event)'
>
<slot name="trigger"></slot>
{#if icon}
<span class='wire-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
{/if}
{#if label}
<span class="wire-dropdown__trigger-label">{label}</span>
{/if}
{#if showChevron}
<span class="icon-[lucide--chevron-down] wire-dropdown__chevron" aria-hidden="true"></span>
{/if}
</button>
{#if closeOnOutside}
<button
type="button"
class="wire-dropdown__dismiss-layer"
data-show='{open || visible}'
aria-label="Close menu"
@click='hideMenu("outside", event)'
></button>
{/if}
<div
class="wire-dropdown__panel"
data-show='{open || visible}'
role="menu"
aria-label='{menuLabel}'
>
<slot name="header"></slot>
<div class="wire-dropdown__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wire-dropdown__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wire-dropdown__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
role="menuitem"
data-dropdown-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
class="wire-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-dropdown__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.badge}
<span class="wire-dropdown__badge">{item.badge}</span>
{:else if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
{:else if item.external}
<span class="icon-[lucide--arrow-up-right] wire-dropdown__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
role="menuitem"
data-dropdown-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wire-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-dropdown__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.badge}
<span class="wire-dropdown__badge">{item.badge}</span>
{:else if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
{:empty}
<div class="wire-dropdown__empty">{emptyLabel}</div>
{/each}
</div>
<slot></slot>
<slot name="footer"></slot>
</div>
</div>
}
style {
.wire-dropdown {
--dropdown-accent: var(--wire-color-primary);
--dropdown-soft: var(--wire-color-primary-soft);
position: relative;
display: inline-flex;
min-width: 0;
}
.wire-dropdown[data-color="secondary"] {
--dropdown-accent: var(--wire-color-secondary);
--dropdown-soft: var(--wire-color-secondary-soft);
}
.wire-dropdown[data-color="info"] {
--dropdown-accent: var(--wire-color-info);
--dropdown-soft: var(--wire-color-info-soft);
}
.wire-dropdown[data-color="success"] {
--dropdown-accent: var(--wire-color-success);
--dropdown-soft: var(--wire-color-success-soft);
}
.wire-dropdown[data-color="warning"] {
--dropdown-accent: var(--wire-color-warning);
--dropdown-soft: var(--wire-color-warning-soft);
}
.wire-dropdown[data-color="danger"] {
--dropdown-accent: var(--wire-color-danger);
--dropdown-soft: var(--wire-color-danger-soft);
}
.wire-dropdown__trigger {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.55rem;
min-height: 2.55rem;
padding: 0.65rem 0.9rem;
color: var(--wire-color-text);
background: var(--wire-color-surface-raised);
border: 1px solid var(--wire-color-border);
border-radius: 0.78rem;
font: inherit;
font-size: 0.84rem;
font-weight: 600;
cursor: pointer;
transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease;
}
.wire-dropdown__trigger:hover,
.wire-dropdown__trigger:focus-visible,
.wire-dropdown[data-open="true"] .wire-dropdown__trigger {
color: var(--dropdown-accent);
background: var(--dropdown-soft);
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border));
outline: none;
}
.wire-dropdown__trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.wire-dropdown__trigger-icon,
.wire-dropdown__chevron {
width: 1rem;
height: 1rem;
}
.wire-dropdown__chevron {
transition: transform 160ms ease;
}
.wire-dropdown[data-open="true"] .wire-dropdown__chevron {
transform: rotate(180deg);
}
.wire-dropdown[data-size="sm"] .wire-dropdown__trigger {
min-height: 2.2rem;
padding: 0.5rem 0.72rem;
font-size: 0.78rem;
}
.wire-dropdown[data-size="lg"] .wire-dropdown__trigger {
min-height: 2.9rem;
padding: 0.78rem 1.05rem;
font-size: 0.9rem;
}
.wire-dropdown__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1100;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wire-dropdown__panel {
position: absolute;
z-index: 1101;
top: calc(100% + 0.55rem);
left: 0;
width: max-content;
min-width: 12rem;
max-width: min(22rem, calc(100vw - 1.5rem));
padding: 0.45rem;
color: var(--wire-color-text);
background:
linear-gradient(145deg, color-mix(in srgb, var(--dropdown-accent) 5%, transparent), transparent 60%),
color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wire-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 24px 64px color-mix(in srgb, black 22%, transparent);
backdrop-filter: blur(18px);
}
.wire-dropdown[data-width="trigger"] .wire-dropdown__panel {
width: 100%;
min-width: 100%;
}
.wire-dropdown[data-width="sm"] .wire-dropdown__panel {
width: 12rem;
}
.wire-dropdown[data-width="md"] .wire-dropdown__panel {
width: 17rem;
}
.wire-dropdown[data-width="lg"] .wire-dropdown__panel {
width: 22rem;
}
.wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel {
right: 0;
left: auto;
}
.wire-dropdown[data-placement="top-start"] .wire-dropdown__panel {
top: auto;
bottom: calc(100% + 0.55rem);
}
.wire-dropdown[data-placement="top-end"] .wire-dropdown__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.55rem);
left: auto;
}
.wire-dropdown[data-variant="soft"] .wire-dropdown__panel {
background:
linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
var(--wire-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wire-dropdown[data-variant="outline"] .wire-dropdown__panel {
background: var(--wire-color-surface-raised);
box-shadow: none;
}
.wire-dropdown__items {
display: grid;
gap: 0.15rem;
}
.wire-dropdown__section-label {
padding: 0.55rem 0.72rem 0.3rem;
color: var(--wire-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wire-dropdown__divider {
height: 1px;
margin: 0.3rem 0.4rem;
background: var(--wire-color-border);
}
.wire-dropdown__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.7rem;
width: 100%;
min-width: 0;
padding: 0.68rem 0.72rem;
color: var(--wire-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
}
.wire-dropdown__item:hover,
.wire-dropdown__item:focus-visible,
.wire-dropdown__item[data-selected="true"] {
color: var(--dropdown-accent);
background: var(--dropdown-soft);
outline: none;
}
.wire-dropdown__item:active {
transform: scale(0.985);
}
.wire-dropdown__item[data-danger="true"] {
color: var(--wire-color-danger);
}
.wire-dropdown__item[data-danger="true"]:hover,
.wire-dropdown__item[data-danger="true"]:focus-visible {
background: var(--wire-color-danger-soft);
}
.wire-dropdown__item[disabled],
.wire-dropdown__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wire-dropdown__item-icon,
.wire-dropdown__status {
width: 1rem;
height: 1rem;
}
.wire-dropdown__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wire-dropdown__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-dropdown__copy small {
overflow: hidden;
color: var(--wire-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-dropdown__badge {
padding: 0.18rem 0.45rem;
color: var(--dropdown-accent);
background: var(--dropdown-soft);
border-radius: 999px;
font-size: 0.64rem;
font-weight: 700;
}
.wire-dropdown kbd {
padding: 0.16rem 0.38rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.36rem;
font-size: 0.64rem;
font-family: inherit;
}
.wire-dropdown__empty {
padding: 1rem;
color: var(--wire-color-text-muted);
font-size: 0.8rem;
text-align: center;
}
@media (max-width: 639px) {
.wire-dropdown__panel {
position: fixed;
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
min-width: 0;
max-width: none;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-dropdown__trigger,
.wire-dropdown__chevron,
.wire-dropdown__item {
transition: none;
}
}
}
}
+55
View File
@@ -0,0 +1,55 @@
component FileInput {
outputs {
input(payload: { files: File[]; name: string; sourceEvent: Event })
change(payload: { files: File[]; name: string; sourceEvent: Event })
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { files: File[]; name: string; sourceEvent: Event })
clear(payload: { name: string; sourceEvent: Event })
invalid(payload: { message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "File"
hiddenLabel: boolean = false
placeholder: string = "Choose a file"
value: string = ""
icon: string = "icon-[lucide--upload]"
iconPosition: string = "start"
accept: string = ""
multiple: boolean = false
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state selectedName: string = ""
functions {
client function handleChange(sourceEvent) {
sourceEvent.stopPropagation()
selectedName = sourceEvent.currentTarget.files && sourceEvent.currentTarget.files.length ? sourceEvent.currentTarget.files[0].name : ""
output.input({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
output.change({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
if (selectedName) output.select({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
else output.clear({ name: name, sourceEvent: sourceEvent })
}
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ name: name, sourceEvent: sourceEvent }) }
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--file-input {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__file-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<span>{selectedName || value || placeholder}</span><input id="{id || name}" type="file" name="{name}" accept="{accept}" multiple="{multiple}" disabled="{disabled}" required="{required}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="handleChange(event)" @change="handleChange(event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" /></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
+29
View File
@@ -0,0 +1,29 @@
component FileUpload {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
upload(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
progress(payload: { progress: number; [key: string]: string | number | boolean | null | object } | number)
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
cancel(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
remove(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "File Upload"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -0,0 +1,40 @@
component FileUploadProgress {
outputs {
cancel(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
retry(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
complete(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
label: string = "Progress"
value: number = 50
max: number = 100
showValue: boolean = true
fileName: string = ""
fileSize: string = ""
uploadedSize: string = ""
status: string = "uploading"
cancelLabel: string = "Cancel upload"
retryLabel: string = "Retry upload"
class: string = ""
}
state currentValue = value
state currentStatus = status
functions {
shared function percent() { if (!Number(max)) { return 0 } return Math.max(0, Math.min(100, Math.round(Number(currentValue) / Number(max) * 100))) }
client function detail(sourceEvent) { return { value: currentValue, max: max, percent: percent(), status: currentStatus, fileName: fileName, sourceEvent: sourceEvent } }
client function cancelUpload(sourceEvent) { currentStatus = "cancelled"; output.cancel(detail(sourceEvent)) }
client function retryUpload(sourceEvent) { currentValue = 0; currentStatus = "uploading"; output.retry(detail(sourceEvent)) }
client function completeUpload(sourceEvent) { currentValue = max; currentStatus = "complete"; output.complete(detail(sourceEvent)) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload-progress {class}" data-status="{currentStatus}" aria-live="polite">
<div class="wire-next__row"><span>{#if fileName}<strong>{fileName}</strong><small>{#if uploadedSize || fileSize}{uploadedSize}{#if uploadedSize && fileSize} of {/if}{fileSize}{:else}{label}{/if}</small>{:else}<strong>{label}</strong>{/if}</span>{#if showValue}<strong>{percent()}%</strong>{/if}</div>
<progress value="{currentValue}" max="{max}" aria-label="{label}"></progress>
<div class="wire-next__upload-status"><span class="wire-next__upload-status-icon" aria-hidden="true"></span><span>{#if currentStatus === "uploading"}Uploading securely…{:else if currentStatus === "complete"}Upload complete{:else if currentStatus === "error"}Upload failed{:else}Upload cancelled{/if}</span></div>
<div class="wire-next__upload-actions">{#if currentStatus === "uploading"}<button type="button" @click="cancelUpload(event)">{cancelLabel}</button><button type="button" @click="completeUpload(event)">Complete</button>{/if}{#if currentStatus === "error" || currentStatus === "cancelled"}<button type="button" @click="retryUpload(event)">{retryLabel}</button>{/if}</div>
</div>
}
}
+47
View File
@@ -0,0 +1,47 @@
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 = ""
}
view {
<figure
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='rounded'
>
{#if src}
<img
src='{src}'
alt='{alt}'
width='{width}'
height='{height}'
loading='{loading}'
decoding="async"
class="block h-auto w-full object-cover transition duration-300"
class:aspect-square='size === "square"'
class:aspect-video='size === "video"'
class:aspect-[4/3]='size === "landscape"'
class:aspect-[3/4]='size === "portrait"'
/>
{:else}
<div
class="flex min-h-48 w-full items-center justify-center text-[var(--wire-color-text-muted)]"
role="img"
aria-label='{alt || "Image placeholder"}'
>
<span class="icon-[lucide--image] size-8" aria-hidden="true"></span>
</div>
{/if}
<slot></slot>
</figure>
}
}
+54
View File
@@ -0,0 +1,54 @@
component InputGroup {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
submit(payload: { value: string | number | boolean | null | object; name: string; sourceEvent: Event })
action(payload: { name: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Input group"
hiddenLabel: boolean = false
value: string = ""
placeholder: string = ""
type: string = "text"
startText: string = ""
endText: string = ""
icon: string = ""
iconPosition: string = "start"
actionLabel: string = ""
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
functions {
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
client function handleKeydown(sourceEvent) { if (sourceEvent.key === "Enter") { sourceEvent.stopPropagation(); output.submit({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) } }
client function handleAction(sourceEvent) { output.action({ name: name, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--input-group {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__input-group-control">
{#if startText}<span class="wire-next__input-addon">{startText}</span>{/if}
{#if icon}<span class="{icon} wire-next__field-icon" data-position="{iconPosition}" aria-hidden="true"></span>{/if}
<input id="{id || name}" type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" readonly="{readonly}" disabled="{disabled}" required="{required}" aria-invalid="{error ? 'true' : 'false'}" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @keydown="handleKeydown(event)" />
{#if endText}<span class="wire-next__input-addon">{endText}</span>{/if}
{#if actionLabel}<button type="button" disabled="{disabled}" @click="handleAction(event)">{actionLabel}</button>{/if}
</div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
+626
View File
@@ -0,0 +1,626 @@
component InputNumber {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
increment(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
decrement(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
variant: string = "default"
class: string = ""
id: string = ""
name: string = "quantity"
value: number = 0
min: string = ""
max: string = ""
step: number = 1
precision: string = "auto"
label: string = ""
description: string = ""
helpText: string = ""
error: string = ""
invalid: boolean = false
prefix: string = ""
suffix: string = ""
placeholder: string = ""
autocomplete: string = "off"
inputMode: string = "decimal"
ariaLabel: string = ""
required: boolean = false
disabled: boolean = false
inputDisabled: boolean = false
buttonsDisabled: boolean = false
readonly: boolean = false
allowInput: boolean = true
keyboard: boolean = true
wheel: boolean = false
clamp: boolean = true
fullWidth: boolean = false
showButtons: boolean = true
showValidationMessage: boolean = true
decrementLabel: string = "Decrease value"
incrementLabel: string = "Increase value"
controlsLabel: string = "Quantity controls"
requiredMessage: string = "A value is required."
minMessage: string = "Value is below the minimum."
maxMessage: string = "Value is above the maximum."
}
state currentValue = value
state committedValue = value
functions {
shared function inputId() {
if (id !== "") {
return id
}
if (name !== "") {
return name
}
return "input-number"
}
shared function descriptionId() {
return inputId() + "-description"
}
shared function messageId() {
return inputId() + "-message"
}
shared function componentColor() {
if (color === "secondary") {
return "var(--wire-color-secondary)"
}
if (color === "success") {
return "var(--wire-color-success)"
}
if (color === "warning") {
return "var(--wire-color-warning)"
}
if (color === "danger") {
return "var(--wire-color-danger)"
}
if (color === "info") {
return "var(--wire-color-info)"
}
return "var(--wire-color-primary)"
}
shared function hasMin() {
return min !== "" && min !== null && min !== undefined
}
shared function hasMax() {
return max !== "" && max !== null && max !== undefined
}
shared function isBlank() {
return currentValue === "" || currentValue === null || currentValue === undefined
}
shared function normalizedStep() {
return Number(step) > 0 ? Number(step) : 1
}
shared function inferredPrecision() {
if (precision !== "auto" && precision !== "") {
return Math.max(0, Number(precision) || 0)
}
if (String(normalizedStep()).includes(".")) {
return String(normalizedStep()).split(".")[1].length
}
return 0
}
shared function precisionFactor() {
return Math.pow(10, inferredPrecision())
}
shared function roundValue(nextValue) {
return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor()
}
shared function clampValue(nextValue) {
nextValue = Number(nextValue)
if (hasMin() && nextValue < Number(min)) {
nextValue = Number(min)
}
if (hasMax() && nextValue > Number(max)) {
nextValue = Number(max)
}
return roundValue(nextValue)
}
shared function isBelowMin() {
return !isBlank() && hasMin() && Number(currentValue) < Number(min)
}
shared function isAboveMax() {
return !isBlank() && hasMax() && Number(currentValue) > Number(max)
}
shared function isInvalid() {
return (
invalid ||
error !== "" ||
(required && isBlank()) ||
isBelowMin() ||
isAboveMax()
)
}
shared function validationMessage() {
if (error !== "") {
return error
}
if (required && isBlank()) {
return requiredMessage
}
if (isBelowMin()) {
return minMessage
}
if (isAboveMax()) {
return maxMessage
}
return ""
}
shared function hasMessage() {
return (
helpText !== "" ||
(showValidationMessage && isInvalid() && validationMessage() !== "")
)
}
shared function describedBy() {
if (description !== "" && hasMessage()) {
return descriptionId() + " " + messageId()
}
if (description !== "") {
return descriptionId()
}
if (hasMessage()) {
return messageId()
}
return ""
}
shared function decrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMin() && !isBlank() && Number(currentValue) <= Number(min))
)
}
shared function incrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMax() && !isBlank() && Number(currentValue) >= Number(max))
)
}
client function dispatchInputNumberEvent(
sourceEvent,
eventName,
action,
previousValue,
root,
customEvent
) {
root = sourceEvent.currentTarget.closest("[data-wrn-input-number]")
if (!root && sourceEvent.target) {
root = sourceEvent.target.closest("[data-wrn-input-number]")
}
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent(eventName, true, false, {
component: "InputNumber",
name: name,
value: currentValue,
previousValue: previousValue,
action: action,
min: hasMin() ? Number(min) : null,
max: hasMax() ? Number(max) : null,
step: normalizedStep(),
valid: !isInvalid()
})
root.dispatchEvent(customEvent)
}
client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
previousValue = currentValue
currentValue = clampValue(nextValue)
committedValue = currentValue
dispatchInputNumberEvent(
sourceEvent,
"input",
action,
previousValue
)
dispatchInputNumberEvent(
sourceEvent,
"change",
action,
previousValue
)
dispatchInputNumberEvent(
sourceEvent,
action,
action,
previousValue
)
}
client function incrementValue(sourceEvent, nextValue) {
if (incrementDisabled()) {
return
}
if (isBlank()) {
nextValue = hasMin() ? Number(min) : normalizedStep()
} else {
nextValue =
Number(currentValue) +
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
}
applyControlValue(nextValue, "increment", sourceEvent)
}
client function decrementValue(sourceEvent, nextValue) {
if (decrementDisabled()) {
return
}
if (isBlank()) {
nextValue = hasMax() ? Number(max) : -normalizedStep()
} else {
nextValue =
Number(currentValue) -
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
}
applyControlValue(nextValue, "decrement", sourceEvent)
}
client function handleInput(sourceEvent, previousValue, nextValue) {
sourceEvent.stopPropagation()
previousValue = currentValue
nextValue = sourceEvent.target.value
if (nextValue === "") {
currentValue = ""
} else if (!Number.isNaN(Number(nextValue))) {
currentValue = roundValue(Number(nextValue))
}
dispatchInputNumberEvent(
sourceEvent,
"input",
"input",
previousValue
)
}
client function handleChange(sourceEvent, previousValue) {
sourceEvent.stopPropagation()
previousValue = committedValue
if (!isBlank()) {
currentValue = clamp
? clampValue(currentValue)
: roundValue(currentValue)
}
committedValue = currentValue
dispatchInputNumberEvent(
sourceEvent,
"change",
"change",
previousValue
)
}
client function handleKeydown(sourceEvent) {
if (
!keyboard ||
disabled ||
readonly ||
inputDisabled ||
!allowInput
) {
return
}
if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
incrementValue(sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
decrementValue(sourceEvent)
} else if (sourceEvent.key === "Home" && hasMin()) {
sourceEvent.preventDefault()
applyControlValue(Number(min), "decrement", sourceEvent)
} else if (sourceEvent.key === "End" && hasMax()) {
sourceEvent.preventDefault()
applyControlValue(Number(max), "increment", sourceEvent)
}
}
client function handleWheel(sourceEvent) {
if (
!wheel ||
disabled ||
readonly ||
inputDisabled ||
!allowInput
) {
return
}
sourceEvent.preventDefault()
if (sourceEvent.deltaY < 0) {
incrementValue(sourceEvent)
} else if (sourceEvent.deltaY > 0) {
decrementValue(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-wrn-input-number
data-variant='{variant}'
data-size='{size}'
data-color='{color}'
data-value='{currentValue}'
data-invalid='{isInvalid() ? "true" : "false"}'
data-disabled='{disabled ? "true" : "false"}'
style='--input-number-accent: {componentColor()};'
class='relative flex flex-col gap-1.5 text-[var(--wire-color-text)] {fullWidth ? "w-full" : variant === "compact" ? "w-fit max-w-full" : "w-full max-w-sm"} {disabled ? "opacity-60" : ""} {class}'
>
{#if label !== "" && variant !== "labeled" && variant !== "seat"}
<label
for='{inputId()}'
class='inline-flex items-center gap-1 text-sm font-semibold leading-5 text-[var(--wire-color-text)]'
>
<span>{label}</span>
{#if required}
<span
aria-hidden="true"
class='text-[var(--wire-color-danger)]'
>*</span>
{/if}
</label>
{/if}
{#if description !== "" && variant !== "labeled" && variant !== "seat"}
<p
id='{descriptionId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
>
{description}
</p>
{/if}
<div
class='group flex min-w-0 overflow-hidden border bg-[var(--wire-color-surface)] text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] transition-[border-color,box-shadow,background-color] duration-[var(--wire-motion-base)] ease-[var(--wire-ease-standard)] focus-within:border-[var(--input-number-accent)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--input-number-accent)_18%,transparent)] {variant === "compact" ? "rounded-full" : "rounded-[var(--wire-radius-sm)]"} {size === "xs" ? "min-h-8 text-xs" : size === "sm" ? "min-h-9 text-sm" : size === "lg" ? "min-h-12 text-base" : size === "xl" ? "min-h-14 text-lg" : "min-h-10 text-sm"} {isInvalid() ? "border-[var(--wire-color-danger)] focus-within:border-[var(--wire-color-danger)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--wire-color-danger)_18%,transparent)]" : "border-[var(--wire-color-border)]"} {disabled ? "cursor-not-allowed bg-[var(--wire-color-surface-2)]" : ""}'
>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='inline-flex shrink-0 items-center justify-center border-r border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
></span>
</button>
{/if}
<div
class='flex min-w-0 flex-1 {variant === "labeled" ? "flex-col items-stretch justify-center gap-0.5" : variant === "seat" ? "items-center justify-between gap-3" : "items-center"} {size === "xs" ? "px-2" : size === "sm" ? "px-2.5" : size === "lg" ? "px-4" : size === "xl" ? "px-5" : "px-3"}'
>
{#if variant === "labeled" || variant === "seat"}
<div class='min-w-0 flex-1'>
{#if label !== ""}
<label
for='{inputId()}'
class='block truncate font-semibold leading-4 text-[var(--wire-color-text)] {variant === "labeled" ? "text-xs font-medium text-[var(--wire-color-muted)]" : "text-sm"}'
>
{label}
{#if required}
<span
aria-hidden="true"
class='ml-0.5 text-[var(--wire-color-danger)]'
>*</span>
{/if}
</label>
{/if}
{#if description !== ""}
<span
id='{descriptionId()}'
class='block truncate text-xs leading-4 text-[var(--wire-color-muted)]'
>
{description}
</span>
{/if}
</div>
{/if}
<div
class='flex min-w-0 items-center {variant === "seat" ? "w-auto shrink-0" : "w-full"}'
>
{#if prefix !== ""}
<span
aria-hidden="true"
class='shrink-0 pr-1.5 text-[var(--wire-color-muted)]'
>
{prefix}
</span>
{/if}
<input
id='{inputId()}'
name='{name}'
type="number"
value='{currentValue}'
min='{min}'
max='{max}'
step='{normalizedStep()}'
placeholder='{placeholder}'
autocomplete='{autocomplete}'
inputmode='{inputMode}'
aria-label='{ariaLabel !== "" ? ariaLabel : label !== "" ? label : name}'
aria-describedby='{describedBy()}'
aria-invalid='{isInvalid() ? "true" : "false"}'
aria-required='{required ? "true" : "false"}'
aria-disabled='{disabled || inputDisabled ? "true" : "false"}'
required='{required}'
disabled='{disabled}'
readonly='{readonly || inputDisabled || !allowInput}'
tabindex='{inputDisabled ? "-1" : "0"}'
@input='handleInput(event)'
@change='handleChange(event)'
@keydown='handleKeydown(event)'
@wheel='handleWheel(event)'
class='min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-medium leading-none text-[var(--wire-color-text)] outline-none placeholder:text-[var(--wire-color-muted)] read-only:cursor-default disabled:cursor-not-allowed [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none {variant === "horizontal" || variant === "compact" || variant === "seat" ? "text-center" : "text-left"} {variant === "seat" ? "w-10 flex-none" : "w-full"}'
/>
{#if suffix !== ""}
<span
aria-hidden="true"
class='shrink-0 pl-1.5 text-[var(--wire-color-muted)]'
>
{suffix}
</span>
{/if}
</div>
</div>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='inline-flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
></span>
</button>
{:else}
{#if showButtons}
<div
role="group"
aria-label='{controlsLabel}'
class='flex shrink-0 border-l border-[var(--wire-color-border)] {variant === "vertical" ? "flex-col" : "flex-row"}'
>
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1 border-b border-[var(--wire-color-border)]" : "border-r border-[var(--wire-color-border)]"} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
></span>
</button>
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1" : ""} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
></span>
</button>
</div>
{/if}
{/if}
</div>
{#if showValidationMessage && isInvalid() && validationMessage() !== ""}
<p
id='{messageId()}'
role="alert"
aria-live="polite"
class='m-0 flex items-center gap-1.5 text-xs leading-5 text-[var(--wire-color-danger)]'
>
<span
aria-hidden="true"
class='icon-[lucide--circle-alert] size-3.5 shrink-0'
></span>
<span>{validationMessage()}</span>
</p>
{/if}
{#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
<p
id='{messageId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
>
{helpText}
</p>
{/if}
</div>
}
}
+9
View File
@@ -0,0 +1,9 @@
component Kbd {
props {
size: string = "default"
color: string = "primary"
label: string = "⌘ K"
class: string = ""
}
view { <kbd class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--kbd {class}"><slot>{label}</slot></kbd> }
}
+19
View File
@@ -0,0 +1,19 @@
component LayoutSplitter {
outputs {
resizeStart(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
resize(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
resizeEnd(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
class: string = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--layout-splitter wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
}
}
@@ -0,0 +1,24 @@
component LegendIndicator {
outputs {
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Legend Indicator"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--legend-indicator 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>
}
}
+47
View File
@@ -0,0 +1,47 @@
component Link {
props {
size: string = "default"
color: string = "primary"
label: string = "Link"
href: string = "#"
target: string = ""
rel: string = ""
external: boolean = false
class: string = ""
}
view {
<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>
}
}
+107
View File
@@ -0,0 +1,107 @@
component List {
props {
size: string = "default"
color: string = "primary"
title: string = "List"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<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>
}
}
+24
View File
@@ -0,0 +1,24 @@
component ListGroup {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "List Group"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--list-group 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>
}
}
+85
View File
@@ -0,0 +1,85 @@
component Map {
props {
size: string = "default"
color: string = "primary"
title: string = "Map"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<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>
}
}
+111
View File
@@ -0,0 +1,111 @@
component Marquee {
props {
size: string = "default"
color: string = "primary"
title: string = "Marquee"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
state paused: boolean = false
view {
<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;
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
component MegaMenu {
outputs {
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Mega Menu"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
class: string = ""
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--mega-menu wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
}
}
+608
View File
@@ -0,0 +1,608 @@
component Modal {
outputs {
open(payload: { sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
cancel(payload: { sourceEvent: Event })
confirm(payload: { sourceEvent: Event })
}
props {
open: boolean = false
defaultOpen: boolean = false
title: string = "Modal"
description: string = ""
icon: string = ""
label: string = "Modal dialog"
size: string = "md"
placement: string = "center"
color: string = "primary"
variant: string = "default"
showClose: boolean = true
closeLabel: string = "Close modal"
closeOnBackdrop: boolean = true
closeOnEscape: boolean = true
closeOnCancel: boolean = true
closeOnConfirm: boolean = false
showFooter: boolean = true
cancelLabel: string = "Cancel"
cancelIcon: string = ""
confirmLabel: string = "Confirm"
confirmIcon: string = ""
confirmDisabled: boolean = false
confirmLoading: boolean = false
destructive: boolean = false
triggerLabel: string = ""
triggerIcon: string = ""
scrollable: boolean = true
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showModal(sourceEvent) {
visible = true
output.open({ sourceEvent: sourceEvent })
}
client function hideModal(reason, sourceEvent) {
visible = false
output.close({
reason: reason,
sourceEvent: sourceEvent
})
}
client function cancelModal(sourceEvent) {
output.cancel({ sourceEvent: sourceEvent })
if (closeOnCancel) {
hideModal("cancel", sourceEvent)
}
}
client function confirmModal(sourceEvent) {
if (confirmDisabled || confirmLoading) {
return
}
output.confirm({ sourceEvent: sourceEvent })
if (closeOnConfirm) {
hideModal("confirm", sourceEvent)
}
}
client function handleKeydown(sourceEvent) {
if (closeOnEscape && sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
cancelModal(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-ui-component="Modal"
data-open='{open || visible ? "true" : "false"}'
data-size='{size}'
data-placement='{placement}'
data-color='{color}'
data-variant='{variant}'
data-scrollable='{scrollable ? "true" : "false"}'
data-destructive='{destructive ? "true" : "false"}'
class='wire-modal {class}'
>
{#if triggerLabel}
<button
type="button"
class="wire-modal__trigger"
aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}'
@click='showModal(event)'
>
{#if triggerIcon}
<span
class='{triggerIcon}'
aria-hidden="true"
>
</span>
{/if}
<span>{triggerLabel}</span>
</button>
{/if}
<span
class="wire-modal__trigger-slot"
@click='showModal(event)'
>
<slot
name="trigger"
>
</slot>
</span>
<div
class="wire-modal__layer"
data-show='{open || visible}'
role="presentation"
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wire-modal__backdrop"
aria-label='{closeLabel}'
@click='if (closeOnBackdrop) { cancelModal(event) }'
>
</button>
<section
class="wire-modal__panel"
role="dialog"
aria-modal="true"
aria-label='{label || title}'
tabindex="-1"
>
<header
class="wire-modal__header"
>
<div
class="wire-modal__heading"
>
{#if icon}
<span
class='wire-modal__icon {icon}'
aria-hidden="true"
>
</span>
{/if}
<div
class="wire-modal__heading-copy"
>
<slot
name="header"
>
</slot>
{#if title}
<h2>{title}</h2>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
</div>
{#if showClose}
<button
type="button"
class="wire-modal__close"
aria-label='{closeLabel}'
@click='hideModal("close-button", event)'
>
<span
class="icon-[lucide--x]"
aria-hidden="true"
>
</span>
</button>
{/if}
</header>
<div
class="wire-modal__body"
>
<slot></slot>
</div>
{#if showFooter}
<footer
class="wire-modal__footer"
>
<div
class="wire-modal__custom-footer"
>
<slot
name="footer"
>
</slot>
</div>
<div
class="wire-modal__actions"
>
{#if cancelLabel}
<button
type="button"
class="wire-modal__button wire-modal__button--secondary"
@click='cancelModal(event)'
>
{#if cancelIcon}
<span
class='{cancelIcon}'
aria-hidden="true"
>
</span>
{/if}
<span>{cancelLabel}</span>
</button>
{/if}
{#if confirmLabel}
<button
type="button"
class="wire-modal__button wire-modal__button--primary"
disabled='{confirmDisabled || confirmLoading}'
@click='confirmModal(event)'
>
{#if confirmLoading}
<span
class="wire-modal__spinner"
aria-hidden="true"
>
</span>
{:else if confirmIcon}
<span
class='{confirmIcon}'
aria-hidden="true"
>
</span>
{/if}
<span>{confirmLabel}</span>
</button>
{/if}
</div>
</footer>
{/if}
</section>
</div>
</div>
}
style {
.wire-modal {
--modal-accent: var(--wire-color-primary);
--modal-soft: var(--wire-color-primary-soft);
--modal-contrast: var(--wire-color-primary-contrast);
position: relative;
display: inline-flex;
}
.wire-modal[data-color="secondary"] {
--modal-accent: var(--wire-color-secondary);
--modal-soft: var(--wire-color-secondary-soft);
--modal-contrast: var(--wire-color-secondary-contrast);
}
.wire-modal[data-color="info"] {
--modal-accent: var(--wire-color-info);
--modal-soft: var(--wire-color-info-soft);
--modal-contrast: var(--wire-color-info-contrast);
}
.wire-modal[data-color="success"] {
--modal-accent: var(--wire-color-success);
--modal-soft: var(--wire-color-success-soft);
--modal-contrast: var(--wire-color-success-contrast);
}
.wire-modal[data-color="warning"] {
--modal-accent: var(--wire-color-warning);
--modal-soft: var(--wire-color-warning-soft);
--modal-contrast: var(--wire-color-warning-text);
}
.wire-modal[data-color="danger"],
.wire-modal[data-destructive="true"] {
--modal-accent: var(--wire-color-danger);
--modal-soft: var(--wire-color-danger-soft);
--modal-contrast: var(--wire-color-on-danger);
}
.wire-modal__trigger,
.wire-modal__trigger-slot {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.wire-modal__trigger {
appearance: none;
min-height: 2.55rem;
padding: 0.65rem 1rem;
color: var(--modal-contrast);
background: var(--modal-accent);
border: 0;
border-radius: 0.8rem;
font: inherit;
font-size: 0.85rem;
font-weight: 650;
cursor: pointer;
}
.wire-modal__layer {
position: fixed;
inset: 0;
z-index: 1250;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.wire-modal[data-placement="top"] .wire-modal__layer {
align-items: flex-start;
padding-top: clamp(1rem, 8vh, 5rem);
}
.wire-modal__backdrop {
position: absolute;
inset: 0;
z-index: 0;
appearance: none;
padding: 0;
background: color-mix(in srgb, black 62%, transparent);
border: 0;
backdrop-filter: blur(9px);
}
.wire-modal__panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(32rem, calc(100vw - 2rem));
max-height: min(88vh, 52rem);
color: var(--wire-color-text);
background:
radial-gradient(
circle at 100% 0%,
color-mix(in srgb, var(--modal-accent) 9%, transparent),
transparent 34%
),
var(--wire-color-surface-raised);
border: 1px solid color-mix(in srgb, var(--modal-accent) 18%, var(--wire-color-border));
border-radius: 1.35rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 40px 110px color-mix(in srgb, black 38%, transparent);
overflow: hidden;
}
.wire-modal[data-size="sm"] .wire-modal__panel {
width: min(25rem, calc(100vw - 2rem));
}
.wire-modal[data-size="lg"] .wire-modal__panel {
width: min(44rem, calc(100vw - 2rem));
}
.wire-modal[data-size="xl"] .wire-modal__panel {
width: min(64rem, calc(100vw - 2rem));
}
.wire-modal[data-size="full"] .wire-modal__panel {
width: calc(100vw - 2rem);
height: calc(100vh - 2rem);
max-height: none;
}
.wire-modal[data-variant="soft"] .wire-modal__panel {
background:
linear-gradient(145deg, var(--modal-soft), transparent 68%),
var(--wire-color-surface-raised);
}
.wire-modal[data-variant="outline"] .wire-modal__panel {
background: var(--wire-color-surface-raised);
box-shadow: 0 24px 72px color-mix(in srgb, black 24%, transparent);
}
.wire-modal[data-variant="solid"] .wire-modal__panel {
color: var(--modal-contrast);
background: var(--modal-accent);
border-color: color-mix(in srgb, white 20%, transparent);
}
.wire-modal__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.4rem 1.4rem 1.15rem;
border-bottom: 1px solid var(--wire-color-border);
}
.wire-modal__heading {
display: flex;
align-items: flex-start;
gap: 0.9rem;
min-width: 0;
}
.wire-modal__icon {
flex: 0 0 auto;
width: 1.3rem;
height: 1.3rem;
margin-top: 0.15rem;
color: var(--modal-accent);
}
.wire-modal[data-variant="solid"] .wire-modal__icon {
color: currentColor;
}
.wire-modal__heading-copy {
display: grid;
gap: 0.3rem;
min-width: 0;
}
.wire-modal__heading-copy h2,
.wire-modal__heading-copy p {
margin: 0;
}
.wire-modal__heading-copy h2 {
font-size: 1.08rem;
font-weight: 650;
line-height: 1.3;
}
.wire-modal__heading-copy p {
color: var(--wire-color-text-muted);
font-size: 0.8rem;
line-height: 1.55;
}
.wire-modal[data-variant="solid"] .wire-modal__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
.wire-modal__close {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 2.35rem;
height: 2.35rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.75rem;
cursor: pointer;
}
.wire-modal__close:hover,
.wire-modal__close:focus-visible {
color: var(--modal-accent);
border-color: color-mix(in srgb, var(--modal-accent) 34%, var(--wire-color-border));
outline: none;
}
.wire-modal__body {
flex: 1 1 auto;
min-height: 0;
padding: 1.4rem;
}
.wire-modal[data-scrollable="true"] .wire-modal__body {
overflow: auto;
overscroll-behavior: contain;
}
.wire-modal__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.4rem 1.4rem;
border-top: 1px solid var(--wire-color-border);
}
.wire-modal__custom-footer:empty {
display: none;
}
.wire-modal__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.65rem;
margin-left: auto;
}
.wire-modal__button {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 2.55rem;
padding: 0.65rem 1rem;
border-radius: 0.78rem;
font: inherit;
font-size: 0.83rem;
font-weight: 650;
cursor: pointer;
transition: transform 150ms ease, opacity 150ms ease, border-color 150ms ease;
}
.wire-modal__button:active {
transform: scale(0.985);
}
.wire-modal__button--secondary {
color: var(--wire-color-text);
background: transparent;
border: 1px solid var(--wire-color-border);
}
.wire-modal__button--primary {
color: var(--modal-contrast);
background: var(--modal-accent);
border: 1px solid transparent;
}
.wire-modal__button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.wire-modal__spinner {
width: 0.95rem;
height: 0.95rem;
border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
border-top-color: currentColor;
border-radius: 50%;
animation: wire-modal-spin 750ms linear infinite;
}
@keyframes wire-modal-spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 639px) {
.wire-modal__layer {
align-items: flex-end;
padding: 0;
}
.wire-modal__panel,
.wire-modal[data-size="sm"] .wire-modal__panel,
.wire-modal[data-size="lg"] .wire-modal__panel,
.wire-modal[data-size="xl"] .wire-modal__panel {
width: 100%;
max-height: 92vh;
border-radius: 1.25rem 1.25rem 0 0;
}
.wire-modal__footer {
align-items: stretch;
flex-direction: column;
}
.wire-modal__actions {
width: 100%;
}
.wire-modal__button {
flex: 1 1 0;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-modal__button,
.wire-modal__spinner {
animation: none;
transition: none;
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
component Nav {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Nav"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
class: string = ""
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--nav wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
}
}
+143
View File
@@ -0,0 +1,143 @@
component Navbar {
outputs {
toggle(payload: { open: boolean })
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { item: string | number | boolean | null | object; value: string | number | boolean; level: string })
action(payload: { item: string | number | boolean | null | object; value: string | number | boolean })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Primary navigation"
topbarLabel: string = "Utility navigation"
brand: Record<string, unknown> = {}
items: unknown[] = []
actions: unknown[] = []
active: string = ""
sticky: boolean = false
openOnHover: boolean = false
maxWidth: string = "full"
mobileLabel: string = "Toggle navigation"
class: string = ""
}
state mobileOpen: boolean = false
functions {
client function toggleNavigation() {
mobileOpen = !mobileOpen
output.toggle({ open: mobileOpen })
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
}
client function selectItem(item, level) {
mobileOpen = false
output.select({ item: item, value: item.value || "", level: level })
}
client function selectAction(item) {
mobileOpen = false
output.action({ item: item, value: item.value || "" })
}
shared 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)
})
}
client function toggleDropdown(event, item) {
output[event.currentTarget.open ? "open" : "close"]({
source: "menu",
item: item
})
}
}
view {
<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>
}
}
+23
View File
@@ -0,0 +1,23 @@
component Pagination {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Pagination"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
class: string = ""
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--pagination wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
}
}
+117
View File
@@ -0,0 +1,117 @@
component PinInput {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
paste(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Verification code"
name: string = "pin"
value: string = ""
length: number = 4
pattern: string = "[0-9]"
type: string = "text"
inputMode: string = "numeric"
placeholder: string = "○"
autocomplete: string = "one-time-code"
masked: boolean = false
disabled: boolean = false
readonly: boolean = false
required: boolean = false
autoFocus: boolean = false
autoSubmit: boolean = false
allowPaste: boolean = true
clearable: boolean = true
clearLabel: string = "Clear code"
separator: string = ""
groupSize: number = 0
helpText: string = ""
invalid: boolean = false
validationMessage: string = ""
class: string = ""
}
functions {
shared function inputIndexes() {
return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].slice(
0,
Math.max(1, Math.min(12, Number(length)))
)
}
}
view {
<fieldset
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--pin-input {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-wrn-pin-input
data-length="{length}"
data-pattern="{pattern}"
data-allow-paste="{allowPaste ? 'true' : 'false'}"
data-auto-submit="{autoSubmit ? 'true' : 'false'}"
data-value="{value}"
disabled="{disabled}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
>
<div class="wire-next__pin-heading">
<legend>{label}</legend>
<button
type="button"
class="wire-next__pin-clear"
data-pin-clear
aria-label="{clearLabel}"
title="{clearLabel}"
style="{clearable && value ? '' : 'display: none'}"
>
<span class="icon-[lucide--rotate-ccw]" aria-hidden="true"></span>
<span>{clearLabel}</span>
</button>
</div>
<div class="wire-next__pin-cells" role="group" aria-label="{label}">
{#each inputIndexes() as index}
<input
data-pin-cell
data-pin-index="{index}"
id="{name}-{index}"
type="{masked ? 'password' : type}"
inputmode="{inputMode}"
maxlength="1"
value="{value[index] || ''}"
placeholder="{placeholder}"
autocomplete="{index === 0 ? autocomplete : 'off'}"
aria-label="{label} digit {index + 1} of {length}"
aria-required="{required ? 'true' : 'false'}"
readonly="{readonly}"
disabled="{disabled}"
autofocus="{autoFocus && index === 0}"
/>
{#if separator && groupSize !== 0 && (index + 1) % groupSize === 0 && index + 1 !== length}
<span class="wire-next__pin-separator" aria-hidden="true">{separator}</span>
{/if}
{/each}
</div>
<input
type="hidden"
name="{name}"
value="{value}"
required="{required}"
data-pin-value
/>
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
data-error="{name}"
>{validationMessage}</small>
</fieldset>
}
}
+536
View File
@@ -0,0 +1,536 @@
component Popover {
outputs {
toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
open(payload: { sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
action(payload: { href: string; sourceEvent: Event })
}
props {
open: boolean = false
defaultOpen: boolean = false
triggerLabel: string = "Open popover"
triggerIcon: string = ""
title: string = ""
description: string = ""
icon: string = ""
placement: string = "bottom-start"
width: string = "md"
size: string = "default"
color: string = "primary"
variant: string = "raised"
showArrow: boolean = true
showClose: boolean = false
closeLabel: string = "Close popover"
closeOnOutside: boolean = true
closeOnEscape: boolean = true
actionLabel: string = ""
actionHref: string = ""
actionIcon: string = ""
closeOnAction: boolean = true
disabled: boolean = false
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showPopover(sourceEvent) {
if (disabled) {
return
}
visible = true
output.open({ sourceEvent: sourceEvent })
output.toggle({ open: true, sourceEvent: sourceEvent })
}
client function hidePopover(reason, sourceEvent) {
visible = false
output.close({ reason: reason, sourceEvent: sourceEvent })
output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
}
client function togglePopover(sourceEvent) {
if (isOpen()) {
hidePopover("toggle", sourceEvent)
} else {
showPopover(sourceEvent)
}
}
client function activateAction(sourceEvent) {
output.action({
href: actionHref,
sourceEvent: sourceEvent
})
if (closeOnAction) {
hidePopover("action", sourceEvent)
}
}
client function handleKeydown(sourceEvent) {
if (closeOnEscape && sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hidePopover("escape", sourceEvent)
}
}
}
view {
<div
{...attrs}
data-ui-component="Popover"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-width='{width}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wire-popover {class}'
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wire-popover__trigger"
disabled='{disabled}'
aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}'
@click='togglePopover(event)'
>
<slot name="trigger"></slot>
{#if triggerIcon}
<span class='{triggerIcon}' aria-hidden="true"></span>
{/if}
{#if triggerLabel}
<span>{triggerLabel}</span>
{/if}
</button>
{#if closeOnOutside}
<button
type="button"
class="wire-popover__dismiss-layer"
data-show='{open || visible}'
aria-label='{closeLabel}'
@click='hidePopover("outside", event)'
></button>
{/if}
<section
class="wire-popover__panel"
data-show='{open || visible}'
role="dialog"
aria-label='{title || triggerLabel}'
>
{#if showArrow}
<span class="wire-popover__arrow" aria-hidden="true"></span>
{/if}
{#if title || description || icon || showClose}
<header class="wire-popover__header">
<div class="wire-popover__heading">
{#if icon}
<span class='wire-popover__icon {icon}' aria-hidden="true"></span>
{/if}
<div class="wire-popover__heading-copy">
<slot name="header"></slot>
{#if title}
<h3>{title}</h3>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
</div>
{#if showClose}
<button
type="button"
class="wire-popover__close"
aria-label='{closeLabel}'
@click='hidePopover("close-button", event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</header>
{/if}
<div class="wire-popover__body">
<slot></slot>
</div>
{#if actionLabel}
<footer class="wire-popover__footer">
{#if actionHref}
<a
href='{actionHref}'
class="wire-popover__action"
@click='activateAction(event)'
>
<span>{actionLabel}</span>
{#if actionIcon}
<span class='{actionIcon}' aria-hidden="true"></span>
{:else}
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
class="wire-popover__action"
@click='activateAction(event)'
>
<span>{actionLabel}</span>
{#if actionIcon}
<span class='{actionIcon}' aria-hidden="true"></span>
{:else}
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
{/if}
</button>
{/if}
</footer>
{/if}
<slot name="footer"></slot>
</section>
</div>
}
style {
.wire-popover {
--popover-accent: var(--wire-color-primary);
--popover-soft: var(--wire-color-primary-soft);
--popover-contrast: var(--wire-color-primary-contrast);
position: relative;
display: inline-flex;
}
.wire-popover[data-color="secondary"] {
--popover-accent: var(--wire-color-secondary);
--popover-soft: var(--wire-color-secondary-soft);
--popover-contrast: var(--wire-color-secondary-contrast);
}
.wire-popover[data-color="info"] {
--popover-accent: var(--wire-color-info);
--popover-soft: var(--wire-color-info-soft);
--popover-contrast: var(--wire-color-info-contrast);
}
.wire-popover[data-color="success"] {
--popover-accent: var(--wire-color-success);
--popover-soft: var(--wire-color-success-soft);
--popover-contrast: var(--wire-color-success-contrast);
}
.wire-popover[data-color="warning"] {
--popover-accent: var(--wire-color-warning);
--popover-soft: var(--wire-color-warning-soft);
--popover-contrast: var(--wire-color-warning-text);
}
.wire-popover[data-color="danger"] {
--popover-accent: var(--wire-color-danger);
--popover-soft: var(--wire-color-danger-soft);
--popover-contrast: var(--wire-color-on-danger);
}
.wire-popover__trigger {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.55rem;
min-height: 2.55rem;
padding: 0.65rem 0.9rem;
color: var(--wire-color-text);
background: var(--wire-color-surface-raised);
border: 1px solid var(--wire-color-border);
border-radius: 0.78rem;
font: inherit;
font-size: 0.84rem;
font-weight: 600;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
}
.wire-popover__trigger:hover,
.wire-popover__trigger:focus-visible,
.wire-popover[data-open="true"] .wire-popover__trigger {
color: var(--popover-accent);
background: var(--popover-soft);
border-color: color-mix(in srgb, var(--popover-accent) 38%, var(--wire-color-border));
outline: none;
}
.wire-popover__trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.wire-popover[data-size="sm"] .wire-popover__trigger {
min-height: 2.2rem;
padding: 0.5rem 0.72rem;
font-size: 0.78rem;
}
.wire-popover[data-size="lg"] .wire-popover__trigger {
min-height: 2.9rem;
padding: 0.78rem 1.05rem;
font-size: 0.9rem;
}
.wire-popover__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1120;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wire-popover__panel {
position: absolute;
z-index: 1121;
top: calc(100% + 0.72rem);
left: 0;
width: 18rem;
max-width: calc(100vw - 1.5rem);
color: var(--wire-color-text);
background:
linear-gradient(145deg, color-mix(in srgb, var(--popover-accent) 5%, transparent), transparent 62%),
color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
border: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 24px 64px color-mix(in srgb, black 22%, transparent);
backdrop-filter: blur(18px);
}
.wire-popover[data-width="sm"] .wire-popover__panel {
width: 14rem;
}
.wire-popover[data-width="lg"] .wire-popover__panel {
width: 24rem;
}
.wire-popover[data-width="xl"] .wire-popover__panel {
width: 32rem;
}
.wire-popover[data-width="trigger"] .wire-popover__panel {
width: 100%;
min-width: 100%;
}
.wire-popover[data-placement="bottom-end"] .wire-popover__panel {
right: 0;
left: auto;
}
.wire-popover[data-placement="top-start"] .wire-popover__panel {
top: auto;
bottom: calc(100% + 0.72rem);
}
.wire-popover[data-placement="top-end"] .wire-popover__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.72rem);
left: auto;
}
.wire-popover[data-placement="left"] .wire-popover__panel {
top: 50%;
right: calc(100% + 0.72rem);
left: auto;
transform: translateY(-50%);
}
.wire-popover[data-placement="right"] .wire-popover__panel {
top: 50%;
left: calc(100% + 0.72rem);
transform: translateY(-50%);
}
.wire-popover[data-variant="soft"] .wire-popover__panel {
background:
linear-gradient(145deg, var(--popover-soft), transparent 70%),
var(--wire-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wire-popover[data-variant="outline"] .wire-popover__panel {
background: var(--wire-color-surface-raised);
box-shadow: none;
}
.wire-popover[data-variant="solid"] .wire-popover__panel {
color: var(--popover-contrast);
background: var(--popover-accent);
border-color: color-mix(in srgb, white 20%, transparent);
}
.wire-popover__arrow {
position: absolute;
top: -0.35rem;
left: 1.25rem;
width: 0.7rem;
height: 0.7rem;
background: inherit;
border-top: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
border-left: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
transform: rotate(45deg);
}
.wire-popover[data-placement="bottom-end"] .wire-popover__arrow {
right: 1.25rem;
left: auto;
}
.wire-popover[data-placement="top-start"] .wire-popover__arrow,
.wire-popover[data-placement="top-end"] .wire-popover__arrow {
top: auto;
bottom: -0.35rem;
transform: rotate(225deg);
}
.wire-popover[data-placement="left"] .wire-popover__arrow {
top: 50%;
right: -0.35rem;
left: auto;
transform: translateY(-50%) rotate(135deg);
}
.wire-popover[data-placement="right"] .wire-popover__arrow {
top: 50%;
left: -0.35rem;
transform: translateY(-50%) rotate(-45deg);
}
.wire-popover__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.8rem;
padding: 1rem 1rem 0.8rem;
border-bottom: 1px solid var(--wire-color-border);
}
.wire-popover__heading {
display: flex;
align-items: flex-start;
gap: 0.7rem;
min-width: 0;
}
.wire-popover__icon {
flex: 0 0 auto;
width: 1.05rem;
height: 1.05rem;
margin-top: 0.15rem;
color: var(--popover-accent);
}
.wire-popover[data-variant="solid"] .wire-popover__icon {
color: currentColor;
}
.wire-popover__heading-copy {
display: grid;
gap: 0.2rem;
min-width: 0;
}
.wire-popover__heading-copy h3,
.wire-popover__heading-copy p {
margin: 0;
}
.wire-popover__heading-copy h3 {
font-size: 0.9rem;
font-weight: 650;
}
.wire-popover__heading-copy p {
color: var(--wire-color-text-muted);
font-size: 0.74rem;
line-height: 1.5;
}
.wire-popover[data-variant="solid"] .wire-popover__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
.wire-popover__close {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 2rem;
height: 2rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.65rem;
cursor: pointer;
}
.wire-popover__body {
padding: 1rem;
color: var(--wire-color-text-muted);
font-size: 0.8rem;
line-height: 1.6;
}
.wire-popover__footer {
padding: 0 1rem 1rem;
}
.wire-popover__action {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
min-height: 2.35rem;
padding: 0.58rem 0.8rem;
color: var(--popover-contrast);
background: var(--popover-accent);
border: 0;
border-radius: 0.7rem;
font: inherit;
font-size: 0.78rem;
font-weight: 650;
text-decoration: none;
cursor: pointer;
}
@media (max-width: 639px) {
.wire-popover__panel {
position: fixed;
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
max-width: none;
transform: none;
}
.wire-popover__arrow {
display: none;
}
}
}
}
+71
View File
@@ -0,0 +1,71 @@
component RangeSlider {
outputs {
input(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
change(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Range"
hiddenLabel: boolean = false
placeholder: string = ""
variant: string = "normal"
icon: string = ""
iconPosition: string = "start"
value: number = 50
min: number = 0
max: number = 100
step: number = 1
showValue: boolean = true
showBounds: boolean = true
showSteps: boolean = false
marks: unknown[] = []
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state currentValue = value
functions {
shared function percent() { if (Number(max) === Number(min)) { return 0 } return (Number(currentValue) - Number(min)) / (Number(max) - Number(min)) * 100 }
client function setValue(nextValue, sourceEvent, commit, detail, root, slider, input, outputElement, progressValue) { if (readonly || disabled) { return } currentValue = Number(nextValue); root = sourceEvent.currentTarget.closest(".wire-next--range-slider"); if (root) { slider = root.querySelector("[role=slider]"); input = root.querySelector("input[name='" + name + "']"); outputElement = root.querySelector("output"); progressValue = (currentValue - Number(min)) / (Number(max) - Number(min)) * 100; if (slider) { slider.setAttribute("aria-valuenow", String(currentValue)); slider.style.setProperty("--wire-range-progress", progressValue + "%") } if (input) { input.value = String(currentValue) } if (outputElement) { outputElement.textContent = String(currentValue) } } detail = { value: currentValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); if (commit) { output.change(detail) } }
client function valueFromPointer(sourceEvent, rect, ratio) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); return Number(min) + ratio * (Number(max) - Number(min)) }
client function beginSlide(sourceEvent, rect, ratio, rawValue) { if (readonly || disabled) { return } sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId); rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function moveSlide(sourceEvent, rect, ratio, rawValue) { if (!sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { return } rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function endSlide(sourceEvent, rect, ratio, rawValue) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); if (sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { sourceEvent.currentTarget.releasePointerCapture(sourceEvent.pointerId) } output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function selectMark(sourceEvent, root, slider, input, outputElement, nextValue, progressValue, detail) { if (readonly || disabled) { return } root = sourceEvent.currentTarget.closest(".wire-next--range-slider"); slider = root.querySelector(".wire-next__custom-slider"); input = root.querySelector(".wire-next__range-value"); outputElement = root.querySelector("output"); nextValue = Number(sourceEvent.currentTarget.dataset.value); currentValue = nextValue; progressValue = (nextValue - Number(min)) / (Number(max) - Number(min)) * 100; slider.setAttribute("aria-valuenow", String(nextValue)); slider.style.setProperty("--wire-range-progress", progressValue + "%"); input.setAttribute("value", String(nextValue)); outputElement.replaceChildren(String(nextValue)); detail = { value: nextValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function handleKey(sourceEvent) { if (sourceEvent.key === "ArrowRight") { sourceEvent.preventDefault(); currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "ArrowLeft") { sourceEvent.preventDefault(); currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "Home") { sourceEvent.preventDefault(); currentValue = Number(min); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "End") { sourceEvent.preventDefault(); currentValue = Number(max); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } }
client function emitFocus(nameEvent, sourceEvent) { output[nameEvent]({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function decreaseValue(sourceEvent) { currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function increaseValue(sourceEvent) { currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--range-slider {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__range-control" data-variant="{variant}" data-icon-position="{iconPosition}" data-show-steps="{showSteps}">
{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}
<button type="button" class="wire-next__range-stepper" aria-label="Decrease {label}" disabled="{disabled || readonly}" @click="decreaseValue(event)"></button>
<div class="wire-next__range-track">
<input id="{id || name}" class="wire-next__sr-only wire-next__range-value" type="number" name="{name}" value="{currentValue}" min="{min}" max="{max}" step="{step}" required="{required}" readonly />
<div class="wire-next__custom-slider" role="slider" tabindex="{disabled ? '-1' : '0'}" aria-label="{hiddenLabel ? label : ''}" aria-valuemin="{min}" aria-valuemax="{max}" aria-valuenow="{currentValue}" aria-disabled="{disabled}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" style="--wire-range-progress: {percent()}%" @pointerdown="beginSlide(event)" @pointermove="moveSlide(event)" @pointerup="endSlide(event)" @pointercancel="endSlide(event)" @keydown="handleKey(event)" @focus="emitFocus('focus', event)" @blur="emitFocus('blur', event)">
<span class="wire-next__slider-fill"></span><span class="wire-next__slider-thumb"></span>
{#if marks.length}<span class="wire-next__slider-marks">{#each marks as mark}<button type="button" data-value="{mark.value}" style="left: {(Number(mark.value) - Number(min)) / (Number(max) - Number(min)) * 100}%" aria-label="{mark.label}" @click="selectMark(event)"></button>{/each}</span>{/if}
</div>
{#if showBounds}<div class="wire-next__range-bounds"><span>{min}</span><span>Step {step}</span><span>{max}</span></div>{/if}
</div>
{#if showValue}<output for="{id || name}">{currentValue}</output>{/if}
<button type="button" class="wire-next__range-stepper" aria-label="Increase {label}" disabled="{disabled || readonly}" @click="increaseValue(event)">+</button>
</div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
+24
View File
@@ -0,0 +1,24 @@
component Rating {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Rating"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--rating 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>
}
}
+21
View File
@@ -0,0 +1,21 @@
component Scrollspy {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Scrollspy"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
class: string = ""
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--scrollspy wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
}
}
+80
View File
@@ -0,0 +1,80 @@
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 = ""
}
state query = value
view {
<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>
}
}
+82
View File
@@ -0,0 +1,82 @@
component Sidebar {
outputs {
toggle(payload: { open: boolean })
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { source: string })
select(payload: { item: string | number | boolean | null | object; value: string | number | boolean; level: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Sidebar"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
mobileLabel: string = "Open navigation"
class: string = ""
}
state mobileOpen: boolean = false
functions {
client function toggleSidebar() {
mobileOpen = !mobileOpen
output.toggle({ open: mobileOpen })
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
}
client function closeSidebar() {
mobileOpen = false
output.close({ source: "mobile" })
}
client function selectItem(item, level) {
mobileOpen = false
output.select({ item: item, value: item.value || "", level: level })
}
}
view {
<div class="wire-sidebar-shell wire-next--color-{color} wire-next--size-{size} {mobileOpen ? 'is-open' : ''} {class}">
<button type="button" class="wire-sidebar-toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleSidebar()">
<span class="icon-[lucide--panel-left-open]" aria-hidden="true"></span>
<span>{label}</span>
</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>
}
}
+24
View File
@@ -0,0 +1,24 @@
component Stepper {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Stepper"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
class: string = ""
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--stepper wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
}
}
+218
View File
@@ -0,0 +1,218 @@
component StrongPassword {
outputs {
input(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
change(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
strength(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Password"
name: string = "password"
value: string = ""
placeholder: string = "Create a strong password"
autocomplete: string = "new-password"
minLength: number = 8
specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"
requireLowercase: boolean = true
requireUppercase: boolean = true
requireNumber: boolean = true
requireSpecialCharacter: boolean = true
showRequirements: boolean = true
presentation: string = "inline"
hintText: string = "Use a unique password you do not use elsewhere."
emptyLabel: string = "Enter a password"
weakLabel: string = "Weak"
fairLabel: string = "Fair"
goodLabel: string = "Good"
strongLabel: string = "Strong"
disabled: boolean = false
readonly: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
class: string = ""
}
state password = value
state detailsOpen: boolean = false
functions {
shared function hasMinimumLength() {
return password.length >= Number(minLength)
}
shared function hasLowercase() {
return password !== password.toUpperCase()
}
shared function hasUppercase() {
return password !== password.toLowerCase()
}
shared function hasNumber() {
return containsCharacterAt("0123456789", 0)
}
shared function hasSpecialCharacter() {
return containsCharacterAt(specialCharactersSet, 0)
}
shared function containsCharacterAt(characters, index) {
if (index >= characters.length) {
return false
}
if (password.includes(characters[index])) {
return true
}
return containsCharacterAt(characters, index + 1)
}
shared function score() {
return (hasMinimumLength() ? 1 : 0) + (requireLowercase && hasLowercase() ? 1 : 0) + (requireUppercase && hasUppercase() ? 1 : 0) + (requireNumber && hasNumber() ? 1 : 0) + (requireSpecialCharacter && hasSpecialCharacter() ? 1 : 0)
}
shared function maximumScore() {
return 1 + (requireLowercase ? 1 : 0) + (requireUppercase ? 1 : 0) + (requireNumber ? 1 : 0) + (requireSpecialCharacter ? 1 : 0)
}
shared function strengthPercent() {
return Math.round(score() / maximumScore() * 100)
}
shared function strengthLevel() {
if (!password) {
return "empty"
}
if (strengthPercent() <= 25) {
return "weak"
}
if (strengthPercent() <= 50) {
return "fair"
}
if (strengthPercent() < 100) {
return "good"
}
return "strong"
}
shared function strengthLabel() {
if (strengthLevel() === "weak") {
return weakLabel
}
if (strengthLevel() === "fair") {
return fairLabel
}
if (strengthLevel() === "good") {
return goodLabel
}
if (strengthLevel() === "strong") {
return strongLabel
}
return emptyLabel
}
}
view {
<div
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--strong-password wire-next--strong-password-{presentation} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-wrn-strong-password
data-strength="{password ? strengthLevel() : 'empty'}"
data-score="{password ? score() : 0}"
>
<label for="{name}-strong-password">{label}</label>
<div class="wire-next__strong-password-anchor">
<input
{...attrs}
id="{name}-strong-password"
type="password"
name="{name}"
value="{password}"
placeholder="{placeholder}"
autocomplete="{autocomplete}"
minlength="{minLength}"
disabled="{disabled}"
readonly="{readonly}"
required="{required}"
aria-invalid="{invalid ? 'true' : 'false'}"
aria-describedby="{validationMessage ? name + '-validation' : hintText ? name + '-hint' : ''}"
@focus="detailsOpen = true"
@blur="detailsOpen = false"
@input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
/>
{#if presentation === "popover"}
<div
class="wire-next__strong-password-popover"
data-open="{detailsOpen ? 'true' : 'false'}"
role="status"
>
<div class="wire-next__strong-password-popover-arrow" aria-hidden="true"></div>
<div class="wire-next__strong-password-details">
<div class="wire-next__strong-password-summary">
<span>Password strength</span>
<strong>{password ? strengthLabel() : emptyLabel}</strong>
</div>
<div
class="wire-next__strong-password-meter"
role="progressbar"
aria-label="Password strength"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{password ? strengthPercent() : 0}"
>
<span style="width: {password ? strengthPercent() : 0}%"></span>
</div>
{#if showRequirements}
<ul class="wire-next__strong-password-requirements">
<li data-met="{password && hasMinimumLength() ? 'true' : 'false'}">
<span aria-hidden="true"></span>At least {minLength} characters
</li>
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
{#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
</ul>
{/if}
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
</div>
</div>
{/if}
</div>
{#if presentation !== "popover"}
<div class="wire-next__strong-password-details">
<div class="wire-next__strong-password-summary">
<span>Password strength</span>
<strong>{password ? strengthLabel() : emptyLabel}</strong>
</div>
<div
class="wire-next__strong-password-meter"
role="progressbar"
aria-label="Password strength"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{password ? strengthPercent() : 0}"
>
<span style="width: {password ? strengthPercent() : 0}%"></span>
</div>
{#if showRequirements}
<ul class="wire-next__strong-password-requirements">
<li data-met="{password && hasMinimumLength() ? 'true' : 'false'}"><span aria-hidden="true"></span>At least {minLength} characters</li>
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
{#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
</ul>
{/if}
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
</div>
{/if}
<small id="{name}-validation" class="wire-next__validation" data-error="{name}">{validationMessage}</small>
</div>
}
}
+19
View File
@@ -0,0 +1,19 @@
component StyledIcon {
props {
size: string = "default"
color: string = "primary"
title: string = "Styled Icon"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--styled-icon 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>
}
}
+90
View File
@@ -0,0 +1,90 @@
component Tabs {
props {
size: string = "default"
color: string = "primary"
label: string = "Tabs"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
class: string = ""
}
state activeValue = active || (items[0] ? (items[0].value || items[0].id || "0") : "")
view {
<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>
}
}
+66
View File
@@ -0,0 +1,66 @@
component TimePicker {
outputs {
input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
focus(payload: { value: string; name: string; sourceEvent: Event })
blur(payload: { value: string; name: string; sourceEvent: Event })
open(payload: { value: string; name: string; sourceEvent: Event })
close(payload: { value: string; name: string; sourceEvent: Event })
invalid(payload: { name: string; message: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Time"
hiddenLabel: boolean = false
value: string = ""
placeholder: string = ""
variant: string = "normal"
icon: string = "icon-[lucide--clock-3]"
iconPosition: string = "end"
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
min: string = ""
max: string = ""
step: string = ""
format: string = "24"
minuteStep: number = 5
hours = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]
minutes = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"]
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state currentValue = value
state selectedHour = value ? value.split(":")[0] : "00"
state selectedMinute = value ? value.split(":")[1] : "00"
state expanded: boolean = false
functions {
client function commit(part, nextValue, sourceEvent, next, detail) { if (part === "hour") { selectedHour = String(nextValue).padStart(2, "0") } if (part === "minute") { selectedMinute = String(nextValue).padStart(2, "0") } next = selectedHour + ":" + selectedMinute; if ((min && next < min) || (max && next > max)) { return } currentValue = next; detail = { value: currentValue, hour: selectedHour, minute: selectedMinute, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function selectTimePart(sourceEvent, root, input, parts, hourValue, minuteValue, next, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--time-picker"); input = root.querySelector(".wire-next__time-value"); parts = String(input.value || value || "00:00").split(":"); hourValue = sourceEvent.currentTarget.dataset.part === "hour" ? sourceEvent.currentTarget.dataset.value : parts[0]; minuteValue = sourceEvent.currentTarget.dataset.part === "minute" ? sourceEvent.currentTarget.dataset.value : parts[1]; next = hourValue + ":" + minuteValue; if ((min && next < min) || (max && next > max)) { return } currentValue = next; input.setAttribute("value", next); trigger = root.querySelector(".wire-next__time-trigger span"); if (trigger) { trigger.replaceChildren(next) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: next, hour: hourValue, minute: minuteValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function openPicker(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function closePicker(sourceEvent) { expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--time-picker {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__field-control" data-icon-position="{iconPosition}">
{#if icon}<span class="{icon} wire-next__field-icon" aria-hidden="true"></span>{/if}
<input id="{id || name}" class="wire-next__sr-only wire-next__time-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" />
<button type="button" class="wire-next__time-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openPicker(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select time"}</span><i class="{icon}" aria-hidden="true"></i></button>
</div>
<div class="wire-next__time-panel" role="dialog" aria-label="{label}"><div><strong>Hour</strong><div class="wire-next__time-options">{#each hours as hour}<button type="button" data-part="hour" data-value="{hour}" aria-pressed="{String(hour) === selectedHour}" @click="selectTimePart(event)">{hour}</button>{/each}</div></div><div><strong>Minute</strong><div class="wire-next__time-options">{#each minutes as minute}<button type="button" data-part="minute" data-value="{minute}" aria-pressed="{String(minute) === selectedMinute}" @click="selectTimePart(event)">{minute}</button>{/each}</div></div><button type="button" class="wire-next__time-done" @click="closePicker(event)">Done</button></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}
<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
+83
View File
@@ -0,0 +1,83 @@
component Timeline {
props {
size: string = "default"
color: string = "primary"
title: string = "Timeline"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<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>
}
}

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