Compare commits
107
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60e38b983c | ||
|
|
5afb2d1875 | ||
|
|
2e42be7b31 | ||
|
|
b508b49058 | ||
|
|
e638c6be6a | ||
|
|
082da38f67 | ||
|
|
5de792f359 | ||
|
|
a0dab308f0 | ||
|
|
dd9b7a289a | ||
|
|
effed1c3cf | ||
|
|
6133d33b8c | ||
|
|
32187a425c | ||
|
|
531bf7b2da | ||
|
|
afe1c413cc | ||
|
|
f68f79786f | ||
|
|
d47c57a953 | ||
|
|
7a968977f6 | ||
|
|
fe9bb9ede0 | ||
|
|
7ba7c8a73f | ||
|
|
8e6dca0751 | ||
|
|
b76053ae93 | ||
|
|
13b198326f | ||
|
|
2c960fc1dc | ||
|
|
ec23dd4c93 | ||
|
|
f73fbb7aba | ||
|
|
0f201c9767 | ||
|
|
9637a47ec7 | ||
|
|
1719059fa0 | ||
|
|
cf06a60f2b | ||
|
|
8fa3d7ecd1 | ||
|
|
bf0d866793 | ||
|
|
e189b2b62f | ||
|
|
6c49991700 | ||
|
|
4d6db0d887 | ||
|
|
78ca2b85d9 | ||
|
|
d3fe0a71aa | ||
|
|
794a7b3378 | ||
|
|
b1086d14e9 | ||
|
|
743275e6fa | ||
|
|
904127687b | ||
|
|
b72f517fa4 | ||
|
|
52842f149d | ||
|
|
1cea44aaf2 | ||
|
|
c54a69532c | ||
|
|
8d044565e3 | ||
|
|
feff30a2b5 | ||
|
|
816594a58b | ||
|
|
db612df6cd | ||
|
|
bca9549f3a | ||
|
|
232d8e6734 | ||
|
|
8f19a5eb2b | ||
|
|
937ccb9e8e | ||
|
|
6859b5c4ff | ||
|
|
3f5ec8696f | ||
|
|
9fd80b7cef | ||
|
|
7d87200e31 | ||
|
|
51286d0fa3 | ||
|
|
905cbce0c1 | ||
|
|
09ebd44b6c | ||
|
|
2e5c0cc953 | ||
|
|
1660044ed2 | ||
|
|
324928cfca | ||
|
|
5c2c6a0a8f | ||
|
|
ff62918dbb | ||
|
|
54a93c1d14 | ||
|
|
b6bd5cfcb7 | ||
|
|
fdf6282aed | ||
|
|
ca8aabf640 | ||
|
|
a90d3683d3 | ||
|
|
d85be6c456 | ||
|
|
8ef6233ef3 | ||
|
|
91e8b2ab05 | ||
|
|
b2e83bc941 | ||
|
|
3e77a621f5 | ||
|
|
a1f671ed5d | ||
|
|
7c584c1d2e | ||
|
|
709a38feb4 | ||
|
|
35cd28aad4 | ||
|
|
de8d792e37 | ||
|
|
64bb0e366a | ||
|
|
e4502f2437 | ||
|
|
504d065003 | ||
|
|
f09341fcfa | ||
|
|
535ad5af6d | ||
|
|
247f360ae7 | ||
|
|
790b81330a | ||
|
|
69020b2555 | ||
|
|
9ed896d2b9 | ||
|
|
8389d9674e | ||
|
|
5112cc1a62 | ||
|
|
30d1632252 | ||
|
|
7a2b58652a | ||
|
|
5e65627305 | ||
|
|
84dc4e06a2 | ||
|
|
c7ab605e85 | ||
|
|
8aa28205e0 | ||
|
|
b56cb8cba5 | ||
|
|
b3a4d80df3 | ||
|
|
7bd4574b08 | ||
|
|
e32d83933e | ||
|
|
5ce2771718 | ||
|
|
68c7b96a9f | ||
|
|
6a9c48a207 | ||
|
|
9a03b7c8d4 | ||
|
|
361129b6ac | ||
|
|
8069541cd9 | ||
|
|
4a82640c5b |
+20
-2
@@ -1,11 +1,29 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "basic-app",
|
||||
"runtimeExecutable": "bun",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"packages/cli/src/index.ts",
|
||||
"dev",
|
||||
"examples/basic-app",
|
||||
"--port=3520"
|
||||
],
|
||||
"port": 3520
|
||||
},
|
||||
{
|
||||
"name": "component-showcase",
|
||||
"runtimeExecutable": "bun",
|
||||
"runtimeArgs": ["run", "--cwd", "examples/component-showcase", "dev"],
|
||||
"port": 3000
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"packages/cli/src/index.ts",
|
||||
"dev",
|
||||
"examples/component-showcase",
|
||||
"--port=3400"
|
||||
],
|
||||
"port": 3400
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.wrnexus/
|
||||
.wrnexus-*/
|
||||
*.log
|
||||
*.db
|
||||
*.db-shm
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
**/dist/
|
||||
**/.wrnexus/**
|
||||
**/.wrnexus-*/**
|
||||
.publish/
|
||||
**/.wirefw/
|
||||
**/.wrnfw/
|
||||
coverage/
|
||||
bun.lock
|
||||
bun.lockb
|
||||
@@ -12,7 +14,7 @@ bun.lockb
|
||||
**/*.gen.ts
|
||||
**/*.generated.d.ts
|
||||
|
||||
# Bundled .wire compiler for the VS Code extension (generated)
|
||||
# Bundled .wrn compiler for the VS Code extension (generated)
|
||||
editors/vscode/src/compiler.cjs
|
||||
editors/vscode/src/language-server.cjs
|
||||
editors/vscode/src/extension.bundle.cjs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5",
|
||||
"@wrnexus/db": "^0.8.5"
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/db": "^0.8.9"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -79,7 +79,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
|
||||
| `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. |
|
||||
| `wrnexus generate mobile` | Scaffold a Capacitor shell for iOS and Android. |
|
||||
| `wrnexus mobile add <package...>` | Install Capacitor plugins and sync native projects. |
|
||||
| `wrnexus eject <name...>` | Copy Wire UI component `.wrn` sources into `app/components/`. |
|
||||
| `wrnexus eject <name...>` | Copy WrNexus UI component `.wrn` sources into `app/components/`. |
|
||||
| `wrnexus db <cmd>` | Database migrations and tooling (see [db](#wrnexus-db)). |
|
||||
| `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). |
|
||||
| `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. |
|
||||
@@ -179,7 +179,7 @@ WrNexus response pages.
|
||||
|
||||
### `wrnexus eject`
|
||||
|
||||
Copies a Wire UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app.
|
||||
Copies a WrNexus UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app.
|
||||
|
||||
```bash
|
||||
wrnexus eject button card modal
|
||||
@@ -322,5 +322,5 @@ export default config;
|
||||
## Requirements / Notes
|
||||
|
||||
- **Bun-only.** The CLI runs on Bun, spawns the Bun binary for the dev child and `bun test`, and the production build uses `Bun.build`. Node is not supported.
|
||||
- Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn` → `.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable Wire UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`.
|
||||
- Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn` → `.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable WrNexus UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`.
|
||||
- Reads `wrnexus.config.ts` for `db` / `databases`, `theme`, `styles`, `seo`, `security`, `i18n`, and `profiles`, and `wrnexus.workspace.ts` for the gateway.
|
||||
|
||||
+18
-18
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.17",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -44,23 +44,23 @@
|
||||
"wrnexus": "./dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5",
|
||||
"@wrnexus/router": "^0.8.5",
|
||||
"@wrnexus/csr": "^0.8.5",
|
||||
"@wrnexus/compiler": "^0.8.5",
|
||||
"@wrnexus/styles": "^0.8.5",
|
||||
"@wrnexus/dev-server": "^0.8.5",
|
||||
"@wrnexus/ui": "^0.8.5",
|
||||
"@wrnexus/validation": "^0.8.5",
|
||||
"@wrnexus/i18n": "^0.8.5",
|
||||
"@wrnexus/mcp": "^0.8.5",
|
||||
"@wrnexus/playground": "^0.8.5",
|
||||
"@wrnexus/db": "^0.8.5",
|
||||
"@wrnexus/authz": "^0.8.5",
|
||||
"@wrnexus/plugin": "^0.8.5",
|
||||
"@wrnexus/syntax": "^0.8.5",
|
||||
"@wrnexus/typecheck": "^0.8.5",
|
||||
"@wrnexus/security": "^0.8.5",
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/router": "^0.8.9",
|
||||
"@wrnexus/csr": "^0.8.16",
|
||||
"@wrnexus/compiler": "^0.8.9",
|
||||
"@wrnexus/styles": "^0.8.10",
|
||||
"@wrnexus/dev-server": "^0.8.16",
|
||||
"@wrnexus/ui": "^0.8.12",
|
||||
"@wrnexus/validation": "^0.8.9",
|
||||
"@wrnexus/i18n": "^0.8.9",
|
||||
"@wrnexus/mcp": "^0.8.8",
|
||||
"@wrnexus/playground": "^0.8.8",
|
||||
"@wrnexus/db": "^0.8.9",
|
||||
"@wrnexus/authz": "^0.8.9",
|
||||
"@wrnexus/plugin": "^0.8.8",
|
||||
"@wrnexus/syntax": "^0.8.8",
|
||||
"@wrnexus/typecheck": "^0.8.8",
|
||||
"@wrnexus/security": "^0.8.8",
|
||||
"selfsigned": "^5.5.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -69,7 +69,7 @@ bun add @wrnexus/compiler
|
||||
|
||||
All exports come from the package root (`@wrnexus/compiler`).
|
||||
|
||||
### `compileWireFile(source: string): string`
|
||||
### `compileWrnFile(source: string): string`
|
||||
|
||||
Compile `.wrn` source to a TypeScript module string. Throws `ParseError` on invalid input. The output is prefixed with a `// compiled from .wrn` comment.
|
||||
|
||||
@@ -115,10 +115,10 @@ class Lexer {
|
||||
|
||||
### Errors
|
||||
|
||||
| Class | Thrown by | Meaning |
|
||||
| ------------ | ------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `ParseError` | `parse`, `compile`, `compileWireFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. |
|
||||
| `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. |
|
||||
| Class | Thrown by | Meaning |
|
||||
| ------------ | ------------------------------------------------ | --------------------------------------------------------------- |
|
||||
| `ParseError` | `parse`, `compile`, `compileWrnFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. |
|
||||
| `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. |
|
||||
|
||||
### AST types
|
||||
|
||||
@@ -143,9 +143,9 @@ Exported type-only symbols describing the parsed tree:
|
||||
Compile a page:
|
||||
|
||||
```ts
|
||||
import { compileWireFile } from "@wrnexus/compiler";
|
||||
import { compileWrnFile } from "@wrnexus/compiler";
|
||||
|
||||
const ts = compileWireFile(`
|
||||
const ts = compileWrnFile(`
|
||||
page Home {
|
||||
state count = 0
|
||||
seo { title = "Home" description = "Welcome" }
|
||||
@@ -201,6 +201,7 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
|
||||
- `@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.
|
||||
- Client functions automatically commit state changed by `setTimeout` callbacks. For other deferred callbacks (observers, third-party APIs, or detached promise callbacks), call the injected `commit()` function after changing local state; returning/awaiting a promise also commits through the normal function boundary.
|
||||
- `seo { key = "value" ... }` — metadata merged into the generated `meta`.
|
||||
- `style { <raw css> }` — inlined page/component stylesheet (repeatable).
|
||||
- `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code.
|
||||
@@ -213,4 +214,4 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
|
||||
## Requirements / Notes
|
||||
|
||||
- Pure TypeScript with no runtime dependencies; runs under **Bun** as part of the WrNexus toolchain (Node is not supported).
|
||||
- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wire*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.
|
||||
- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wrn*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -37,10 +37,10 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/csr": "^0.8.5",
|
||||
"@wrnexus/syntax": "^0.8.5",
|
||||
"@wrnexus/store": "^0.8.5",
|
||||
"@wrnexus/validation": "^0.8.5"
|
||||
"@wrnexus/csr": "^0.8.16",
|
||||
"@wrnexus/syntax": "^0.8.8",
|
||||
"@wrnexus/store": "^0.8.8",
|
||||
"@wrnexus/validation": "^0.8.9"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -37,7 +37,7 @@ The `Context` (`ctx`) is the single value passed to middleware and handlers.
|
||||
| `Context` | type | Per-request object: `req`, `url`, `lang`, `t`, `params`, `locals`, `user?`, `ip?`, `cookies`, `session`, `localStorage`. |
|
||||
| `Next` | type | `() => Promise<Response> \| Response` — invokes the next middleware/handler. |
|
||||
| `Middleware` | type | `(ctx, next) => Promise<Response> \| Response`. Return `next()` to continue, or a `Response` to short-circuit. |
|
||||
| `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (wires up cookies, session, localStorage snapshot). |
|
||||
| `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (connects up cookies, session, localStorage snapshot). |
|
||||
| `withContextHeaders(ctx, res)` | fn | Apply accumulated headers (e.g. `Set-Cookie`) from the context onto a response. |
|
||||
| `PageComponent` | type | `(ctx) => string \| Promise<string>` — a page module's default export. |
|
||||
| `PageMeta` / `SeoConfig` | type | `<head>` metadata: `title`, `description`, `canonical`, `robots`, `image`, `twitterCard`, `themeColor`, … |
|
||||
@@ -70,15 +70,15 @@ cookie-backed `SessionStore`.
|
||||
|
||||
### CSRF — `@wrnexus/core`
|
||||
|
||||
Double-submit cookie pattern: a readable `wire-csrf` cookie is echoed in an
|
||||
Double-submit cookie pattern: a readable `wrn-csrf` cookie is echoed in an
|
||||
`x-csrf-token` header on unsafe requests.
|
||||
|
||||
| Export | Signature | Notes |
|
||||
| ----------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `csrfToken(ctx)` | `(Context) => string` | Ensures the CSRF cookie exists and returns its token. |
|
||||
| `verifyCsrf(ctx)` | `(Context) => boolean` | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). |
|
||||
| `csrfProtection()` | `() => Middleware` | 403s unsafe requests with a missing/mismatched token. |
|
||||
| `CSRF_COOKIE` / `CSRF_HEADER` | `"wire-csrf"` / `"x-csrf-token"` | Cookie & header names. |
|
||||
| Export | Signature | Notes |
|
||||
| ----------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `csrfToken(ctx)` | `(Context) => string` | Ensures the CSRF cookie exists and returns its token. |
|
||||
| `verifyCsrf(ctx)` | `(Context) => boolean` | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). |
|
||||
| `csrfProtection()` | `() => Middleware` | 403s unsafe requests with a missing/mismatched token. |
|
||||
| `CSRF_COOKIE` / `CSRF_HEADER` | `"wrn-csrf"` / `"x-csrf-token"` | Cookie & header names. |
|
||||
|
||||
### Rate limiting — `@wrnexus/core`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -111,11 +111,11 @@ Intercepts same-origin `<a>` clicks, fetches the target page, and swaps the `#ap
|
||||
|
||||
Connects to `/realtime/<name>` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes.
|
||||
|
||||
Programmatic API via `window.wire`:
|
||||
Programmatic API via `window.wrn`:
|
||||
|
||||
```ts
|
||||
wire.room(name): Room // open (or reuse) a room connection
|
||||
wire.bindRooms(root?) // (re)bind declarative [data-room] containers
|
||||
wrn.room(name): Room // open (or reuse) a room connection
|
||||
wrn.bindRooms(root?) // (re)bind declarative [data-room] containers
|
||||
|
||||
interface Room {
|
||||
name: string;
|
||||
@@ -211,7 +211,7 @@ A realtime chat, fully declarative:
|
||||
Or drive a room from code:
|
||||
|
||||
```ts
|
||||
const room = wire.room("lobby");
|
||||
const room = wrn.room("lobby");
|
||||
room.on("chat", (msg) => console.log(msg.user, msg.text));
|
||||
room.send({ type: "chat", user: "ada", text: "hi" });
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.16",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5"
|
||||
"@wrnexus/core": "^0.8.9"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -128,7 +128,7 @@ const events = await getDb("analytics").all("SELECT * FROM hits");
|
||||
|
||||
Migrations are `.sql` files (in e.g. `app/db/migrations`), each split into
|
||||
`-- +up` and `-- +down` sections. A file with no markers is treated entirely as
|
||||
`up`. Applied names are recorded in a `_wire_migrations` table so each runs once.
|
||||
`up`. Applied names are recorded in a `_wrn_migrations` table so each runs once.
|
||||
|
||||
- `parseMigration(name, content)` → `Migration` (`{ name, up, down }`).
|
||||
- `loadMigrations(dir)` — parse all `.sql` files, sorted by filename.
|
||||
@@ -249,7 +249,7 @@ SQL driver — use `@wrnexus/db/mongo` directly.
|
||||
- **Bun-only.** Uses `bun:sqlite` (SQLite adapter + session store) and `Bun.SQL`
|
||||
(Postgres/MySQL). Migrations/scaffolding use `node:fs`/`node:path`.
|
||||
- Works with `@wrnexus/core` — `sqliteSessionStore` implements its
|
||||
`SessionBackend`; `getDb`/`setDb` are wired by the WrNexus runtime from
|
||||
`SessionBackend`; `getDb`/`setDb` are connected by the WrNexus runtime from
|
||||
`wrnexus.config.ts`.
|
||||
- The `mongodb` npm package is an optional, lazily-imported peer — install it
|
||||
only if you use `@wrnexus/db/mongo`. The core package stays dependency-free.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -6,7 +6,7 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
|
||||
|
||||
## Overview
|
||||
|
||||
This package is the server runtime that powers a WrNexus app in both development and production. A single **request runtime** (`createHandlers`) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about _how_ modules and assets are produced, so the dev and prod entry points wire in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app **gateway** (route several apps by `Host` header behind one port) and a portable `node:http` adapter for WinterCG hosts. It is entirely server-side and Bun-native (`Bun.serve`, `Bun.file`, `Bun.gzipSync`).
|
||||
This package is the server runtime that powers a WrNexus app in both development and production. A single **request runtime** (`createHandlers`) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about _how_ modules and assets are produced, so the dev and prod entry points wrn in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app **gateway** (route several apps by `Host` header behind one port) and a portable `node:http` adapter for WinterCG hosts. It is entirely server-side and Bun-native (`Bun.serve`, `Bun.file`, `Bun.gzipSync`).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -21,16 +21,16 @@ bun add @wrnexus/dev-server
|
||||
|
||||
### Main entry (`@wrnexus/dev-server`)
|
||||
|
||||
| Export | Kind | Purpose |
|
||||
| --------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `startServer(opts: ServeOptions)` | `Promise<RunningServer>` | Start the dev server on `Bun.serve`: builds the router, connects/migrates databases, wires assets + HMR, and starts the file watcher. |
|
||||
| `createHandlers(deps: RuntimeDeps)` | `Handlers` | The shared request runtime (fetch + websocket handlers). Re-exported from `runtime.ts`. |
|
||||
| `createProductionServer(manifest, opts)` | `Bun.Server` | Start the production server from a precompiled manifest. |
|
||||
| `createProductionHandlers(manifest, opts)` | `Handlers` | Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam). |
|
||||
| `startGateway(opts: GatewayOptions)` | `Promise<RunningGateway>` | Boot multiple apps as child processes and route by `Host`. |
|
||||
| `toRequest`, `writeResponse`, `nodeListener`, `serveNode` | functions | `node:http` ↔ WinterCG `Request`/`Response` adapter. |
|
||||
| `RESTART_EXIT_CODE` | `number` (`97`) | Exit code the dev child uses to ask the supervisor for a fresh process. |
|
||||
| `STYLES_HREF`, `HMR_CLIENT_JS` | constants | The global stylesheet URL and the inline HMR client script. |
|
||||
| Export | Kind | Purpose |
|
||||
| --------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `startServer(opts: ServeOptions)` | `Promise<RunningServer>` | Start the dev server on `Bun.serve`: builds the router, connects/migrates databases, connects assets + HMR, and starts the file watcher. |
|
||||
| `createHandlers(deps: RuntimeDeps)` | `Handlers` | The shared request runtime (fetch + websocket handlers). Re-exported from `runtime.ts`. |
|
||||
| `createProductionServer(manifest, opts)` | `Bun.Server` | Start the production server from a precompiled manifest. |
|
||||
| `createProductionHandlers(manifest, opts)` | `Handlers` | Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam). |
|
||||
| `startGateway(opts: GatewayOptions)` | `Promise<RunningGateway>` | Boot multiple apps as child processes and route by `Host`. |
|
||||
| `toRequest`, `writeResponse`, `nodeListener`, `serveNode` | functions | `node:http` ↔ WinterCG `Request`/`Response` adapter. |
|
||||
| `RESTART_EXIT_CODE` | `number` (`97`) | Exit code the dev child uses to ask the supervisor for a fresh process. |
|
||||
| `STYLES_HREF`, `HMR_CLIENT_JS` | constants | The global stylesheet URL and the inline HMR client script. |
|
||||
|
||||
Exported types: `ServeOptions`, `RunningServer`, `RuntimeDeps`, `AssetServer`, `WsData`, `GatewayApp`, `GatewayOptions`, `GatewayAuth`, `GatewaySecurity`, `RunningGateway`, `FetchHandler`.
|
||||
|
||||
@@ -85,7 +85,7 @@ interface RuntimeDeps {
|
||||
getMiddleware(): Promise<Middleware[]>;
|
||||
assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
|
||||
hasStyles?: boolean; // inject the global stylesheet link
|
||||
hasUi?: boolean; // inject the Wire UI stylesheet (/__wrnexus/ui.css)
|
||||
hasUi?: boolean; // inject the WrNexus UI stylesheet (/__wrnexus/ui.css)
|
||||
theme?: ResolvedTheme; // enables /__wrnexus/theme.css + <html data-theme>
|
||||
i18n?: ResolvedI18n; // enables ctx.t, <html lang>, {t:key} markers
|
||||
inlineStyles?: string; // inline small prod stylesheets into <head>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.16",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -41,27 +41,27 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/authz": "^0.8.5",
|
||||
"@wrnexus/rpc": "^0.8.5",
|
||||
"@wrnexus/core": "^0.8.5",
|
||||
"@wrnexus/dev-toolbar": "^0.8.5",
|
||||
"@wrnexus/router": "^0.8.5",
|
||||
"@wrnexus/ssr": "^0.8.5",
|
||||
"@wrnexus/csr": "^0.8.5",
|
||||
"@wrnexus/compiler": "^0.8.5",
|
||||
"@wrnexus/styles": "^0.8.5",
|
||||
"@wrnexus/ui": "^0.8.5",
|
||||
"@wrnexus/validation": "^0.8.5",
|
||||
"@wrnexus/i18n": "^0.8.5",
|
||||
"@wrnexus/db": "^0.8.5",
|
||||
"@wrnexus/pubsub": "^0.8.5",
|
||||
"@wrnexus/uploader": "^0.8.5",
|
||||
"@wrnexus/plugin": "^0.8.5",
|
||||
"@wrnexus/store": "^0.8.5",
|
||||
"@wrnexus/security": "^0.8.5",
|
||||
"@wrnexus/observability": "^0.8.5",
|
||||
"@wrnexus/cache": "^0.8.5",
|
||||
"@wrnexus/pwa": "^0.8.5"
|
||||
"@wrnexus/authz": "^0.8.9",
|
||||
"@wrnexus/rpc": "^0.8.9",
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/dev-toolbar": "^0.8.9",
|
||||
"@wrnexus/router": "^0.8.9",
|
||||
"@wrnexus/ssr": "^0.8.9",
|
||||
"@wrnexus/csr": "^0.8.16",
|
||||
"@wrnexus/compiler": "^0.8.9",
|
||||
"@wrnexus/styles": "^0.8.10",
|
||||
"@wrnexus/ui": "^0.8.12",
|
||||
"@wrnexus/validation": "^0.8.9",
|
||||
"@wrnexus/i18n": "^0.8.9",
|
||||
"@wrnexus/db": "^0.8.9",
|
||||
"@wrnexus/pubsub": "^0.8.9",
|
||||
"@wrnexus/uploader": "^0.8.9",
|
||||
"@wrnexus/plugin": "^0.8.8",
|
||||
"@wrnexus/store": "^0.8.8",
|
||||
"@wrnexus/security": "^0.8.8",
|
||||
"@wrnexus/observability": "^0.8.8",
|
||||
"@wrnexus/cache": "^0.8.8",
|
||||
"@wrnexus/pwa": "^0.8.8"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# @wrnexus/encryption
|
||||
|
||||
Authenticated encryption, hashing, HMAC, key rotation, and optional encrypted HTTP exchanges for WRNexusJS.
|
||||
|
||||
## Core helpers
|
||||
|
||||
- `generateKey()` — random 256-bit AES key encoded as base64.
|
||||
- `deriveKey(password, salt)` — PBKDF2-derived AES key.
|
||||
- `encrypt(plaintext, key)` / `decrypt(payload, key)` — AES-256-GCM.
|
||||
- `sha256(data)` — SHA-256 digest.
|
||||
- `hmacSign(data, secret)` / `hmacVerify(...)` — HMAC-SHA256.
|
||||
- `createKeyring(keys)` — active/previous key management.
|
||||
- `seal()` / `open()` — versioned ciphertext with key ID.
|
||||
|
||||
## Encrypted HTTP envelope
|
||||
|
||||
```ts
|
||||
import {
|
||||
createEncryptedRequest,
|
||||
createKeyring,
|
||||
createMemoryReplayStore,
|
||||
decryptEncryptedResponse,
|
||||
encryptedExchange,
|
||||
} from "@wrnexus/encryption";
|
||||
|
||||
const keyring = createKeyring([{ id: "2026-08", secret: process.env.API_BODY_KEY!, active: true }]);
|
||||
|
||||
const replayStore = createMemoryReplayStore();
|
||||
|
||||
// Server middleware.
|
||||
app.use(
|
||||
encryptedExchange({
|
||||
keyring,
|
||||
replayStore,
|
||||
maxAgeMs: 60_000,
|
||||
maxBodyBytes: 1_048_576,
|
||||
}),
|
||||
);
|
||||
|
||||
// Controlled service/native client.
|
||||
const request = await createEncryptedRequest(
|
||||
"https://api.example.com/private/report",
|
||||
{ reportId: "report-1" },
|
||||
{ method: "POST", keyring },
|
||||
);
|
||||
const response = await fetch(request);
|
||||
const result = await decryptEncryptedResponse(response, request, { keyring });
|
||||
```
|
||||
|
||||
The envelope binds authenticated ciphertext to:
|
||||
|
||||
- HTTP method
|
||||
- URL path and query
|
||||
- request ID
|
||||
- timestamp and expiry window
|
||||
- encryption key ID
|
||||
- optional replay-store consumption
|
||||
|
||||
`encryptedBody()` decrypts request bodies only. `encryptedExchange()` also encrypts successful downstream responses while allowing application exceptions to propagate normally. `encryptedFetch()` provides a convenient controlled-client call.
|
||||
|
||||
## Security boundary
|
||||
|
||||
Encrypted HTTP bodies **do not replace TLS/HTTPS**. Always use HTTPS.
|
||||
|
||||
This layer is appropriate for service-to-service traffic, native/mobile applications, controlled agents, and selected fields protected with server-managed keys. It cannot conceal data from an end user when browser JavaScript receives the decryption key. Never ship a long-lived server encryption key to a browser.
|
||||
|
||||
Use a shared replay store such as Redis in multi-instance deployments. The memory replay store is process-local.
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/encryption"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/encryption",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"encryption"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
# @wrnexus/helpers
|
||||
|
||||
Safe convenience helpers for common WrNexus application flows. The package uses
|
||||
standard `Context`, `URL`, and `Response` values and has no runtime dependency beyond
|
||||
`@wrnexus/core`.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @wrnexus/helpers
|
||||
```
|
||||
|
||||
The package is private, so the machine must be authenticated to the `wrnexus` npm
|
||||
organization.
|
||||
|
||||
## Usage
|
||||
|
||||
### Redirect an unauthenticated forward-auth request
|
||||
|
||||
The gateway calls an SSO verifier on a different URL from the original application.
|
||||
These helpers reconstruct the original URL from the gateway headers and safely place it
|
||||
in the login redirect:
|
||||
|
||||
```ts
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { redirectToLogin } from "@wrnexus/helpers";
|
||||
|
||||
export const GET = async (ctx: Context) => {
|
||||
if (await hasValidSession(ctx)) {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
return redirectToLogin(ctx, "/login", {
|
||||
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
This creates a response such as:
|
||||
|
||||
```text
|
||||
Location: http://sso.localhost:3000/login?returnTo=http%3A%2F%2Fadmin.localhost%3A3000%2F
|
||||
```
|
||||
|
||||
Always list the application hosts that are valid redirect destinations. Forwarded host
|
||||
headers are rejected when `allowedHosts` is absent or does not match, preventing an open
|
||||
redirect.
|
||||
|
||||
The SSO hostname is the login destination, not an `allowedHosts` entry. For example,
|
||||
when protecting `admin.localhost:3000`, keep `admin.localhost:3000` in the allowlist even
|
||||
though the verifier runs at `sso.localhost:3000`. WRNexus preserves both hosts across a
|
||||
nested gateway request.
|
||||
|
||||
### Support dynamic tenant domains
|
||||
|
||||
```ts
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { getOriginalRequestOrigin, redirectToLogin } from "@wrnexus/helpers";
|
||||
|
||||
export const GET = async (ctx: Context) => {
|
||||
const allowedHosts = (host: string) => host === "example.test" || host.endsWith(".example.test");
|
||||
|
||||
console.info("Authentication requested by", getOriginalRequestOrigin(ctx, { allowedHosts }));
|
||||
return redirectToLogin(ctx, "https://auth.example.test/login", {
|
||||
allowedHosts,
|
||||
returnToParam: "continue",
|
||||
status: 303,
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- `getOriginalRequestUrl(ctx, options): URL` — reconstruct the gateway URL.
|
||||
- `getOriginalRequestOrigin(ctx, options): string` — return only its origin.
|
||||
- `getOriginalRequestPath(ctx): string` — return its path and query string.
|
||||
- `getOriginalRequestMethod(ctx): string` — return its HTTP method.
|
||||
- `redirectToLogin(ctx, loginUrl, options): Response` — create a login redirect with an
|
||||
encoded `returnTo` parameter.
|
||||
|
||||
For direct requests without gateway headers, URL helpers use `ctx.url`.
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/helpers"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/helpers",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"helpers"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
|
||||
default: "en",
|
||||
locales: ["en", "mr", "hi"],
|
||||
fallbacks: { "mr-IN": ["mr", "en"] },
|
||||
cookie: { name: "wire-lang", sameSite: "Lax", secure: true },
|
||||
cookie: { name: "wrn-lang", sameSite: "Lax", secure: true },
|
||||
});
|
||||
|
||||
const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
|
||||
@@ -57,7 +57,7 @@ Enable `i18nPlugin()` to use:
|
||||
- `<LanguageSwitcher />`
|
||||
- `<LocaleStatus />`
|
||||
|
||||
`LanguageSwitcher` renders a native `select[data-wire-lang]`. The packaged runtime validates the
|
||||
`LanguageSwitcher` renders a native `select[data-wrn-lang]`. The packaged runtime validates the
|
||||
selection against the configured locales, writes the configured language cookie, updates the
|
||||
document `lang`/`dir` attributes, emits `wrnexus:language-change`, and reloads so the next SSR
|
||||
request uses the same cookie. No application-owned browser script is required.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
|
||||
"license": "MIT",
|
||||
@@ -42,9 +42,9 @@
|
||||
"./components/*": "./components/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5",
|
||||
"@wrnexus/plugin": "^0.8.5",
|
||||
"@wrnexus/ui": "^0.8.5"
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/plugin": "^0.8.8",
|
||||
"@wrnexus/ui": "^0.8.12"
|
||||
},
|
||||
"wrnexus": {
|
||||
"plugin": {
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
# @wrnexus/jwt
|
||||
|
||||
> Dependency-free JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WrNexus.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
## Overview
|
||||
|
||||
`@wrnexus/jwt` signs and verifies stateless JSON Web Tokens using the **HS256**
|
||||
(HMAC-SHA-256) algorithm. It has no runtime dependencies — signing and
|
||||
verification are implemented directly on the standard **Web Crypto** API
|
||||
(`crypto.subtle`), which Bun provides natively. It runs server-side and pairs
|
||||
with the session-based auth in `@wrnexus/core`, giving you a stateless option
|
||||
for API and mobile clients. Reach for it when you need bearer-token auth rather
|
||||
than cookie sessions.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @wrnexus/jwt
|
||||
```
|
||||
|
||||
> Private package — the machine must be authenticated to the `wrnexus` npm org
|
||||
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
|
||||
|
||||
## API
|
||||
|
||||
Single entry point (`@wrnexus/jwt`). All functions are async and return Promises.
|
||||
|
||||
| Export | Kind | Description |
|
||||
| --------------------------------------- | --------- | --------------------------------------------------------------- |
|
||||
| `signJwt(payload, secret, options?)` | function | Sign claims into an HS256 token string. |
|
||||
| `verifyJwt<T>(token, secret, options?)` | function | Verify a token and return its claims, or throw. |
|
||||
| `jwtAuth(options)` | function | Middleware that verifies a bearer JWT and sets `ctx.user`. |
|
||||
| `JwtError` | class | Error thrown on any signature/payload/expiry failure. |
|
||||
| `JwtClaims` | interface | Claims shape (`sub`, `iat`, `exp`, `nbf`, plus arbitrary keys). |
|
||||
| `SignOptions` | interface | Options for `signJwt`. |
|
||||
| `JwtAuthOptions` | interface | Options for `jwtAuth`. |
|
||||
|
||||
### `signJwt(payload, secret, options?)`
|
||||
|
||||
```ts
|
||||
function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;
|
||||
```
|
||||
|
||||
Signs `payload` with `secret` using HS256 and returns the encoded token
|
||||
(`header.body.signature`). An `iat` (issued-at) claim is always added.
|
||||
|
||||
`SignOptions`:
|
||||
|
||||
- `expiresIn?: number` — seconds until expiry; sets the `exp` claim.
|
||||
- `now?: number` — override the issued-at time (seconds), useful for testing.
|
||||
|
||||
### `verifyJwt<T>(token, secret, options?)`
|
||||
|
||||
```ts
|
||||
function verifyJwt<T extends JwtClaims = JwtClaims>(
|
||||
token: string,
|
||||
secret: string,
|
||||
options?: { now?: number },
|
||||
): Promise<T>;
|
||||
```
|
||||
|
||||
Verifies the HS256 signature and returns the decoded claims typed as `T`.
|
||||
Throws `JwtError` when the token is malformed, the signature is invalid, the
|
||||
payload is not valid JSON, the token is expired (`exp`), or not yet valid
|
||||
(`nbf`). Pass `now` (seconds) to override the reference time for the `exp`/`nbf`
|
||||
checks.
|
||||
|
||||
### `jwtAuth(options)`
|
||||
|
||||
```ts
|
||||
function jwtAuth(options: JwtAuthOptions): Middleware;
|
||||
```
|
||||
|
||||
Returns a WrNexus `Middleware` that reads a token, verifies it, and assigns the
|
||||
claims to `ctx.user`.
|
||||
|
||||
`JwtAuthOptions`:
|
||||
|
||||
- `secret: string` — the HMAC secret used to verify tokens.
|
||||
- `getToken?: (ctx: Context) => string | undefined` — how to extract the token.
|
||||
Defaults to reading `Authorization: Bearer <token>`.
|
||||
- `required?: boolean` — when `true` (default), a missing or invalid token
|
||||
responds with `401 { ok: false, error: "Unauthorized" }`. When `false`,
|
||||
requests pass through and `ctx.user` is only set if a valid token is present.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { signJwt, verifyJwt, jwtAuth, JwtError } from "@wrnexus/jwt";
|
||||
|
||||
const secret = process.env.JWT_SECRET!;
|
||||
|
||||
// Sign a token that expires in one hour
|
||||
const token = await signJwt({ sub: user.id, role: "admin" }, secret, {
|
||||
expiresIn: 3600,
|
||||
});
|
||||
|
||||
// Verify it later
|
||||
try {
|
||||
const claims = await verifyJwt<{ sub: string; role: string }>(token, secret);
|
||||
console.log(claims.sub, claims.role);
|
||||
} catch (err) {
|
||||
if (err instanceof JwtError) {
|
||||
// invalid signature, expired, malformed, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Protecting routes with the middleware:
|
||||
|
||||
```ts
|
||||
import { jwtAuth } from "@wrnexus/jwt";
|
||||
|
||||
// Require a valid bearer token; ctx.user holds the verified claims
|
||||
app.use(jwtAuth({ secret: process.env.JWT_SECRET! }));
|
||||
|
||||
// Optional auth — populate ctx.user when present, but don't 401
|
||||
app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false }));
|
||||
```
|
||||
|
||||
## Requirements / Notes
|
||||
|
||||
- **Bun-only.** Uses the standard Web Crypto API (`crypto.subtle.importKey`,
|
||||
`sign`, `verify`) plus `btoa`/`atob` and `TextEncoder`/`TextDecoder` — all
|
||||
provided by Bun. No third-party crypto dependency.
|
||||
- **Algorithm:** HS256 (HMAC with SHA-256) only. Asymmetric algorithms (RS/ES)
|
||||
are not supported.
|
||||
- Integrates with [`@wrnexus/core`](../core) for `Context`, `Middleware`, and
|
||||
`ctx.user`; it complements the framework's cookie/session auth with a
|
||||
stateless bearer-token flow for API and mobile clients.
|
||||
|
||||
## Access, refresh, scope, and cookie helpers
|
||||
|
||||
```ts
|
||||
import {
|
||||
createAccessToken,
|
||||
createRefreshToken,
|
||||
verifyAccessToken,
|
||||
verifyRefreshToken,
|
||||
extractBearerToken,
|
||||
requireScopes,
|
||||
jwtCookie,
|
||||
} from "@wrnexus/jwt";
|
||||
```
|
||||
|
||||
The helpers add explicit `type: "access" | "refresh"` claims, scope checks, refresh-token family metadata, no-store token responses, and secure cookie defaults. `__Host-` cookies are rejected unless they use `Path=/` and `Secure`; `SameSite=None` is rejected without `Secure`.
|
||||
|
||||
## 0.8 helper kit
|
||||
|
||||
```ts
|
||||
import {
|
||||
createTokenPair,
|
||||
verifyAccessToken,
|
||||
verifyRefreshToken,
|
||||
extractBearerToken,
|
||||
readJwtCookie,
|
||||
jwtCookie,
|
||||
clearJwtCookie,
|
||||
requireScopes,
|
||||
} from "@wrnexus/jwt";
|
||||
|
||||
const pair = await createTokenPair(user.id, {
|
||||
accessSecret: process.env.JWT_ACCESS_SECRET!,
|
||||
refreshSecret: process.env.JWT_REFRESH_SECRET!,
|
||||
scopes: ["profile:read"],
|
||||
family: sessionFamily,
|
||||
});
|
||||
```
|
||||
|
||||
The helper kit validates `__Host-` cookie invariants, cookie names and paths, `SameSite=None` security, typed access/refresh token types, scope requirements, and no-store token responses.
|
||||
In addition to local HS256 secrets/keyrings, the package verifies standards-based
|
||||
RS256 tokens through bounded remote JWKS caches:
|
||||
|
||||
```ts
|
||||
import { createRemoteJwks, verifyJwtWithJwks } from "@wrnexus/jwt";
|
||||
|
||||
const jwks = createRemoteJwks("https://issuer.example/.well-known/jwks.json");
|
||||
const claims = await verifyJwtWithJwks(token, jwks, {
|
||||
issuer: "https://issuer.example",
|
||||
audience: "my-api",
|
||||
maxAge: 300,
|
||||
});
|
||||
```
|
||||
|
||||
JWKS URLs must use HTTPS. Responses have key-count/byte limits, accept only
|
||||
RS256 signing RSA keys, deduplicate concurrent refreshes, cache imported public
|
||||
keys, and force an immediate refresh for an unknown `kid` so issuer rotation
|
||||
does not wait for cache expiry. Never use decoded-but-unverified claims for an
|
||||
authorization decision.
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/jwt"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/jwt",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"jwt"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
# @wrnexus/mobile
|
||||
|
||||
> SSR-safe access to Capacitor plugins from WRNexusJS browser code.
|
||||
|
||||
## Overview
|
||||
|
||||
`@wrnexus/mobile` keeps optional native imports out of server rendering while giving
|
||||
browser-owned modules one consistent registry for Capacitor plugins. During SSR,
|
||||
`mobile.isNative()` is `false` and `mobile.platform()` is `"web"`.
|
||||
|
||||
## Installation
|
||||
|
||||
Install a plugin through the WRNexusJS CLI so the web and native projects stay aligned:
|
||||
|
||||
```bash
|
||||
wrnexus mobile add @capacitor/camera @capacitor/haptics
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Register and invoke a Capacitor plugin
|
||||
|
||||
Import Capacitor packages only from browser-owned code, never from API routes or SSR
|
||||
helpers.
|
||||
|
||||
```ts
|
||||
import { Camera, CameraResultType } from "@capacitor/camera";
|
||||
import { mobile } from "@wrnexus/mobile";
|
||||
|
||||
mobile.registerPlugin("Camera", Camera);
|
||||
|
||||
export async function takePhoto() {
|
||||
if (!mobile.isNative()) return null;
|
||||
return mobile.invoke("Camera", "getPhoto", {
|
||||
quality: 85,
|
||||
resultType: CameraResultType.Uri,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Provide a browser fallback
|
||||
|
||||
`whenNative` runs the first callback only in a Capacitor WebView and can return a
|
||||
web/SSR-safe fallback everywhere else.
|
||||
|
||||
```ts
|
||||
import { Haptics, ImpactStyle } from "@capacitor/haptics";
|
||||
import { mobile } from "@wrnexus/mobile";
|
||||
|
||||
mobile.registerPlugin("Haptics", Haptics);
|
||||
|
||||
export const confirmAction = () =>
|
||||
mobile.whenNative(
|
||||
() => mobile.invoke("Haptics", "impact", { style: ImpactStyle.Medium }),
|
||||
() => navigator.vibrate?.(30),
|
||||
);
|
||||
```
|
||||
|
||||
### Read an optional plugin without throwing
|
||||
|
||||
```ts
|
||||
import type { NetworkPlugin } from "@capacitor/network";
|
||||
import { mobile } from "@wrnexus/mobile";
|
||||
|
||||
const network = mobile.plugin<NetworkPlugin>("Network");
|
||||
const status = network ? await network.getStatus() : { connected: true, connectionType: "unknown" };
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- `registerPlugin(name, instance)` registers a browser-imported plugin.
|
||||
- `plugin(name)` returns a plugin or `undefined`; `requirePlugin(name)` throws when absent.
|
||||
- `invoke(plugin, method, options?)` calls a registered method and returns its result.
|
||||
- `whenNative(native, fallback?)` selects native behavior without breaking SSR.
|
||||
- `isNative()` and `platform()` report the current Capacitor environment.
|
||||
|
||||
Unavailable required plugins throw `MobileUnavailableError` with an actionable message.
|
||||
|
||||
The package also provides portable application-facing primitives:
|
||||
|
||||
- `listenDeepLinks` normalizes initial and live links with an allowed-scheme list.
|
||||
- `PushNotifications` performs permission gating and validates registrations.
|
||||
- `SecureStorage` namespaces and validates keys over an application-supplied encrypted
|
||||
Keychain/Keystore adapter; it does not mislabel browser `localStorage` as secure.
|
||||
- `OfflineQueue` persists bounded sync batches through a pluggable durable store.
|
||||
|
||||
## Requirements / Notes
|
||||
|
||||
- Capacitor plugin imports must remain in browser-owned modules.
|
||||
- `@wrnexus/mobile` re-exports `native` from `@wrnexus/native` for applications that
|
||||
prefer the higher-level cross-platform capability API.
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/mobile"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/mobile",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"mobile"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/native": "^0.8.5"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
# @wrnexus/native
|
||||
|
||||
> Cross-platform capabilities for browsers, Capacitor WebViews, and compiled native apps.
|
||||
|
||||
## Overview
|
||||
|
||||
`@wrnexus/native` exposes capabilities by name so application code can ask what the
|
||||
current platform supports before presenting an action. Browser capabilities use Web
|
||||
APIs; mobile capabilities use installed Capacitor plugins. `platform()` returns
|
||||
`"server"` during SSR, `"browser"` on the web, and the Capacitor platform in a native
|
||||
WebView.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @wrnexus/native
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Share a page when the platform supports it
|
||||
|
||||
```ts
|
||||
import { native } from "@wrnexus/native";
|
||||
|
||||
export async function shareCurrentPage() {
|
||||
if (!native.supports("share")) return false;
|
||||
await native.run("share", {
|
||||
title: document.title,
|
||||
url: location.href,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Register an application-specific capability
|
||||
|
||||
`register` returns an unregister function, which is useful for tests and temporary
|
||||
feature modules.
|
||||
|
||||
```ts
|
||||
import { native } from "@wrnexus/native";
|
||||
|
||||
const unregister = native.register("orders.scan", {
|
||||
browser: {
|
||||
supported: () => typeof window !== "undefined",
|
||||
run: async ({ orderId }: { orderId: string }) => {
|
||||
const code = window.prompt(`Scan code for order ${orderId}`);
|
||||
return { code };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await native.run<{ code: string | null }>("orders.scan", { orderId: "ord_42" });
|
||||
unregister();
|
||||
```
|
||||
|
||||
### Target browser or mobile behavior explicitly
|
||||
|
||||
```ts
|
||||
import { native } from "@wrnexus/native";
|
||||
|
||||
const canUseMobileCamera = native.supports("camera", "mobile");
|
||||
const position = await native.run(
|
||||
"geolocation",
|
||||
{ enableHighAccuracy: true },
|
||||
{ target: "browser" },
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- `supports(name, target?)` checks availability without running the capability.
|
||||
- `run(name, options?, runOptions?)` executes it or rejects with `NativeUnavailableError`.
|
||||
- `register(name, capability)` adds or overrides a capability and returns cleanup.
|
||||
- `registered()` lists capability names; `clearRegistry()` resets the registry.
|
||||
- `isMobile()` and `platform()` report the current target safely during SSR.
|
||||
|
||||
Built-ins include `camera`, `clipboard.write`, `share`, `geolocation`, `network`,
|
||||
`haptics`, storage, filesystem, notifications, and device information.
|
||||
|
||||
`defineNativeManifest` declares required capabilities and typed permissions, while
|
||||
`PermissionManager` normalizes permission query/request flows across platform adapters.
|
||||
|
||||
## Requirements / Notes
|
||||
|
||||
Use `supports()` before showing optional controls. Mobile capabilities require their
|
||||
matching Capacitor plugins to be installed and registered by the application.
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/native"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/native",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"native"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
},
|
||||
"./browser": {
|
||||
"types": "./dist/browser.d.ts",
|
||||
"import": "./dist/browser.js"
|
||||
},
|
||||
"./mobile": {
|
||||
"types": "./dist/mobile.d.ts",
|
||||
"import": "./dist/mobile.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
# @wrnexus/oauth
|
||||
|
||||
> Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
## Overview
|
||||
|
||||
`@wrnexus/oauth` implements the OAuth 2.0 Authorization Code flow (with PKCE) for
|
||||
server-side sign-in. It ships ready-made provider presets and a `defineProvider`
|
||||
helper for custom providers, then gives you two flow functions — `startAuth`
|
||||
(build the redirect) and `completeAuth` (exchange the code and fetch the user's
|
||||
profile). It has no runtime dependencies: it uses the platform `fetch` and
|
||||
WebCrypto only. Pairs naturally with `@wrnexus/core`'s `logIn` to establish a
|
||||
session once you have a normalized profile.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @wrnexus/oauth
|
||||
```
|
||||
|
||||
> Private package — the machine must be authenticated to the `wrnexus` npm org
|
||||
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
|
||||
|
||||
## API
|
||||
|
||||
### Providers
|
||||
|
||||
Each preset takes `ProviderCredentials` and returns an `OAuthProvider`.
|
||||
|
||||
```ts
|
||||
interface ProviderCredentials {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes?: string[]; // override the preset's default scopes
|
||||
}
|
||||
```
|
||||
|
||||
| Export | Default scopes | Notes |
|
||||
| ------------------------ | ---------------------------- | ------------------------------------------------------------------ |
|
||||
| `google(creds)` | `openid`, `email`, `profile` | Sets `access_type: offline` for refresh tokens. |
|
||||
| `github(creds)` | `read:user`, `user:email` | Maps `name` (falls back to `login`) and `avatar_url`. |
|
||||
| `discord(creds)` | `identify`, `email` | Builds the avatar CDN URL from the user id + hash. |
|
||||
| `defineProvider(config)` | — | Pass a full `OAuthProvider` to define a custom OAuth 2.0 provider. |
|
||||
|
||||
An `OAuthProvider` describes the endpoints, scopes, credentials, optional extra
|
||||
authorize params, and a `mapProfile` normalizer:
|
||||
|
||||
```ts
|
||||
interface OAuthProvider {
|
||||
name: string;
|
||||
authorizeUrl: string;
|
||||
tokenUrl: string;
|
||||
userInfoUrl: string;
|
||||
scopes: string[];
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
authorizeParams?: Record<string, string>; // e.g. access_type, prompt
|
||||
mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
|
||||
}
|
||||
```
|
||||
|
||||
### Flow
|
||||
|
||||
#### `startAuth(provider, options): Promise<StartAuthResult>`
|
||||
|
||||
Builds the authorize redirect URL with a generated PKCE challenge and CSRF
|
||||
`state`. Store the returned `state` and `verifier` (session/cookie), then 302 the
|
||||
user to `url`.
|
||||
|
||||
```ts
|
||||
interface StartAuthOptions {
|
||||
redirectUri: string;
|
||||
state?: string; // reuse a state instead of generating one
|
||||
params?: Record<string, string>; // extra authorize params, merged last
|
||||
}
|
||||
|
||||
interface StartAuthResult {
|
||||
url: string; // authorize URL to redirect to
|
||||
state: string; // CSRF state — verify on callback
|
||||
verifier: string; // PKCE code verifier — pass to completeAuth
|
||||
}
|
||||
```
|
||||
|
||||
#### `completeAuth(provider, options): Promise<{ tokens, profile }>`
|
||||
|
||||
On the callback: exchanges the authorization `code` for tokens, then fetches and
|
||||
normalizes the user profile. Convenience wrapper over `exchangeCode` +
|
||||
`fetchProfile`.
|
||||
|
||||
```ts
|
||||
interface CompleteAuthOptions {
|
||||
code: string;
|
||||
redirectUri: string;
|
||||
verifier?: string; // the PKCE verifier from startAuth
|
||||
fetch?: typeof fetch; // inject a fetch implementation (tests)
|
||||
}
|
||||
```
|
||||
|
||||
#### Lower-level helpers
|
||||
|
||||
| Export | Signature | Purpose |
|
||||
| ---------------------------------------- | ------------------------- | --------------------------------------------------------------- |
|
||||
| `exchangeCode(provider, options)` | `→ Promise<OAuthTokens>` | Exchange an authorization code for tokens. |
|
||||
| `fetchProfile(provider, tokens, fetch?)` | `→ Promise<OAuthProfile>` | Fetch + normalize the user's profile. |
|
||||
| `randomToken(bytes?)` | `→ string` | Random URL-safe token (default 32 bytes) for `state`/verifiers. |
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
interface OAuthTokens {
|
||||
access_token: string;
|
||||
token_type?: string;
|
||||
refresh_token?: string;
|
||||
expires_in?: number;
|
||||
id_token?: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
interface OAuthProfile {
|
||||
id: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { google, startAuth, completeAuth } from "@wrnexus/oauth";
|
||||
import { logIn } from "@wrnexus/core";
|
||||
|
||||
const provider = google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
});
|
||||
|
||||
const redirectUri = "https://example.com/auth/callback";
|
||||
|
||||
// 1. Kick off sign-in: redirect the user to the provider.
|
||||
async function beginLogin(ctx) {
|
||||
const { url, state, verifier } = await startAuth(provider, { redirectUri });
|
||||
// Persist state + verifier in the session, then redirect.
|
||||
ctx.session.set("oauth_state", state);
|
||||
ctx.session.set("oauth_verifier", verifier);
|
||||
return Response.redirect(url, 302);
|
||||
}
|
||||
|
||||
// 2. Handle the callback.
|
||||
async function handleCallback(ctx, code: string, state: string) {
|
||||
if (state !== ctx.session.get("oauth_state")) throw new Error("bad state");
|
||||
|
||||
const { profile } = await completeAuth(provider, {
|
||||
code,
|
||||
redirectUri,
|
||||
verifier: ctx.session.get("oauth_verifier"),
|
||||
});
|
||||
|
||||
logIn(ctx, { id: profile.id, email: profile.email });
|
||||
}
|
||||
```
|
||||
|
||||
Custom provider with `defineProvider`:
|
||||
|
||||
```ts
|
||||
import { defineProvider, startAuth } from "@wrnexus/oauth";
|
||||
|
||||
const gitlab = defineProvider({
|
||||
name: "gitlab",
|
||||
authorizeUrl: "https://gitlab.com/oauth/authorize",
|
||||
tokenUrl: "https://gitlab.com/oauth/token",
|
||||
userInfoUrl: "https://gitlab.com/api/v4/user",
|
||||
scopes: ["read_user"],
|
||||
clientId: process.env.GITLAB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITLAB_CLIENT_SECRET!,
|
||||
mapProfile: (raw) => ({
|
||||
id: String(raw.id),
|
||||
email: raw.email as string | undefined,
|
||||
name: raw.name as string | undefined,
|
||||
avatar: raw.avatar_url as string | undefined,
|
||||
raw,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Requirements / Notes
|
||||
|
||||
- **Bun-only.** Relies on the global `fetch` and WebCrypto (`crypto.getRandomValues`,
|
||||
`crypto.subtle.digest`) — no other runtime dependencies.
|
||||
- The flow is stateless by design: you are responsible for storing `state` and
|
||||
`verifier` between `startAuth` and `completeAuth` (session or signed cookie).
|
||||
- Pairs with [`@wrnexus/core`](../core) — feed the normalized `OAuthProfile` into
|
||||
`logIn` to establish a session.
|
||||
OIDC integrations can combine strict discovery with the rotating JWKS verifier:
|
||||
|
||||
```ts
|
||||
import { createRemoteJwks } from "@wrnexus/jwt";
|
||||
import { discoverOidc, verifyOidcIdToken } from "@wrnexus/oauth";
|
||||
|
||||
const metadata = await discoverOidc("https://issuer.example");
|
||||
const jwks = createRemoteJwks(metadata.jwks_uri);
|
||||
const claims = await verifyOidcIdToken(idToken, {
|
||||
issuer: metadata.issuer,
|
||||
clientId: "client-id",
|
||||
jwks,
|
||||
nonce: expectedNonce,
|
||||
accessToken,
|
||||
});
|
||||
```
|
||||
|
||||
Discovery requires an exact normalized issuer and HTTPS endpoints without URL
|
||||
credentials/fragments. ID-token verification checks the RS256 signature,
|
||||
expiry/not-before, issuer, audience, required OIDC claims, nonce, multi-audience
|
||||
`azp`, optional token age, and optional `at_hash` binding.
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/oauth"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/oauth",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"oauth"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/jwt": "^0.8.5"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
# @wrnexus/plugin
|
||||
|
||||
## Least-privilege package permissions
|
||||
|
||||
Package manifests declare every framework capability they register:
|
||||
|
||||
```json
|
||||
{
|
||||
"wrnexus": {
|
||||
"permissions": ["routes", "migrations"],
|
||||
"routes": [{ "kind": "api", "path": "/api/example", "entry": "./route.ts" }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Applications can enable fail-closed grants:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
pluginPermissions: {
|
||||
enforce: true,
|
||||
grants: { "example-plugin": ["routes"] },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Discovery rejects used-but-undeclared capabilities with
|
||||
`WRN-PLUGIN-PERMISSION-UNDECLARED` and ungranted capabilities with
|
||||
`WRN-PLUGIN-PERMISSION-DENIED`. Permissions cover components, browser runtime,
|
||||
assets, styles, routes, middleware, migrations, config, transforms,
|
||||
diagnostics/tooling, and server/build hooks.
|
||||
|
||||
## Compatibility matrices
|
||||
|
||||
Manifests can add `compatibility: { bunMin: "1.3.0", os: ["linux",
|
||||
"darwin"] }` alongside `runtimes` and `requires`. Use
|
||||
`testPluginCompatibility(manifest, targets)` in a package test to exercise the
|
||||
complete support matrix. Runtime discovery enforces the same Bun minimum, OS,
|
||||
runtime, and capability declarations used by the test kit.
|
||||
|
||||
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.
|
||||
|
||||
## Complete lifecycle and contributions
|
||||
|
||||
Plugins may implement `setup`, `configure`, `configResolved`, `transformAst`,
|
||||
`transformCode`, `diagnostics`, `routes`, `configureServer`, `buildStart`,
|
||||
`buildEnd`, `render`, `deploy`, `shutdown`, and `hmrUpdate`. The runner preserves
|
||||
resolved plugin order for every hook and executes `setup` exactly once.
|
||||
|
||||
In addition to components, routes, middleware, assets, styles, runtimes, and
|
||||
migrations, plugins can contribute `directives`, `cliCommands`,
|
||||
`virtualModules`, `deploymentAdapters`, `configSchemas`, `documentation`, and
|
||||
`typeDefinitions`. Names are collision checked. Configuration schemas run after
|
||||
configuration resolution, CLI commands are callable as normal `wrnexus`
|
||||
commands, directives participate in AST transformation, and production builds
|
||||
materialize virtual modules and invoke matching contributed adapters.
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/plugin",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/plugin — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/plugin"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/plugin",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"plugin"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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.8.5"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -44,6 +44,10 @@
|
||||
"import": "./dist/redis.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/rpc": "^0.8.9"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
# @wrnexus/queue
|
||||
|
||||
> A background job queue with delays, retries + exponential backoff, recurring jobs, and concurrent workers.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
## Overview
|
||||
|
||||
`@wrnexus/queue` is a server-side in-process job queue. You register named
|
||||
workers, enqueue jobs (optionally delayed or recurring), and let the queue poll
|
||||
and run them on a timer — with per-job retry limits and doubling backoff between
|
||||
attempts. The default store lives in memory; the design allows a pluggable driver
|
||||
to back it with Redis/SQL for durability across restarts. Reach for it when you
|
||||
need to defer work (emails, webhooks, cleanup) off the request path without a
|
||||
heavyweight external broker. Tests can drive it deterministically via `drain()`.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @wrnexus/queue
|
||||
```
|
||||
|
||||
> Private package — the machine must be authenticated to the `wrnexus` npm org
|
||||
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
|
||||
|
||||
## API
|
||||
|
||||
The package exports a single factory plus its supporting types.
|
||||
|
||||
### `createQueue(options?): Queue`
|
||||
|
||||
Creates a new queue instance.
|
||||
|
||||
```ts
|
||||
function createQueue(options?: QueueOptions): Queue;
|
||||
```
|
||||
|
||||
#### `QueueOptions`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------------------------------------ | ---------- | -------------------------------------------------------- |
|
||||
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
|
||||
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
|
||||
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
|
||||
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
|
||||
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
|
||||
| `capacity` | `number` | unlimited | Maximum queued plus active jobs before adds reject. |
|
||||
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
|
||||
|
||||
### `Queue`
|
||||
|
||||
The object returned by `createQueue`.
|
||||
|
||||
| Method | Signature | Description |
|
||||
| ---------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `add` | `add<T>(name, data: T, options?: AddOptions): Promise<Job<T>>` | Enqueue a job under a worker name. Returns the created job. |
|
||||
| `process` | `process<T>(name, handler: JobHandler<T>): void` | Register the worker that runs jobs of the given name. |
|
||||
| `drain` | `drain(now?: number): Promise<number>` | Run every job whose `runAt ≤ now`, once. Returns how many ran. |
|
||||
| `start` | `start(): void` | Begin polling every `pollMs`. No-op if already started. |
|
||||
| `stop` | `stop(): void` | Stop the poll timer. |
|
||||
| `shutdown` | `shutdown({ force? }): Promise<void>` | Stop accepting jobs and await active work; force aborts it. |
|
||||
| `size` | `size(): number` | Number of jobs currently queued. |
|
||||
| `get/list` | `get(id)` / `list(name?)` | Inspect defensive copies of pending jobs. |
|
||||
| `cancel` | `cancel(id): boolean` | Remove queued work or abort an active handler. |
|
||||
| `failed` | `failed(): Job[]` | Inspect exhausted jobs in the dead-letter collection. |
|
||||
| `retry` | `retry(id): Promise<boolean>` | Reset and requeue a dead-lettered job. |
|
||||
|
||||
#### `AddOptions`
|
||||
|
||||
| Option | Type | Description |
|
||||
| ---------------- | -------- | -------------------------------------------------------------------------- |
|
||||
| `delayMs` | `number` | Delay before the job becomes runnable (ms). |
|
||||
| `maxAttempts` | `number` | Max attempts before dead-lettering. Defaults to the queue's `maxAttempts`. |
|
||||
| `repeat` | `number` | Re-enqueue this job this many ms after each successful run (recurring). |
|
||||
| `priority` | `number` | Higher values are selected first among due jobs. |
|
||||
| `idempotencyKey` | `string` | Return the matching pending job instead of enqueueing a duplicate. |
|
||||
|
||||
#### `JobHandler<T>`
|
||||
|
||||
```ts
|
||||
type JobHandler<T = unknown> = (
|
||||
job: Job<T>,
|
||||
context: { signal: AbortSignal },
|
||||
) => void | Promise<void>;
|
||||
```
|
||||
|
||||
#### `Job<T>`
|
||||
|
||||
```ts
|
||||
interface Job<T = unknown> {
|
||||
id: string; // e.g. "job_1"
|
||||
name: string;
|
||||
data: T;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
runAt: number; // epoch ms; job runs when now ≥ runAt
|
||||
repeat?: number; // if set, re-enqueue this many ms after each success
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Register workers, enqueue jobs, then start the poller:
|
||||
|
||||
```ts
|
||||
import { createQueue } from "@wrnexus/queue";
|
||||
|
||||
const queue = createQueue({ maxAttempts: 3, backoffMs: 1000 });
|
||||
|
||||
// Register a worker for the "email" job name.
|
||||
queue.process<{ to: string }>("email", async (job) => {
|
||||
await send(job.data.to);
|
||||
});
|
||||
|
||||
// Enqueue a delayed job with up to 3 attempts.
|
||||
await queue.add("email", { to: "a@b.com" }, { delayMs: 5000, maxAttempts: 3 });
|
||||
|
||||
queue.start(); // begin polling; queue.stop() to halt
|
||||
```
|
||||
|
||||
Use `context.signal` in network/database calls so forced shutdown and active
|
||||
cancellation finish promptly. For process termination, prefer
|
||||
`await queue.shutdown()`; use `{ force: true }` only after your grace period.
|
||||
|
||||
### Durable queue
|
||||
|
||||
`createDurableQueue({ store })` retains jobs until their handler succeeds and
|
||||
supports atomic leases when a driver implements `QueueStore.claim`. It exposes
|
||||
the same cancellation/shutdown behavior plus `list`, `failed`, and `retry`.
|
||||
The included `memoryQueueStore()` is useful for tests; production Redis/SQL
|
||||
drivers should make `claim()` atomic to prevent two workers executing one job.
|
||||
|
||||
### Recurring jobs
|
||||
|
||||
Pass `repeat` to re-enqueue a job a fixed interval after each successful run:
|
||||
|
||||
```ts
|
||||
queue.process("heartbeat", async () => ping());
|
||||
await queue.add("heartbeat", {}, { repeat: 60_000 }); // runs ~every minute
|
||||
```
|
||||
|
||||
### Handling permanent failures
|
||||
|
||||
When a job's `attempts` reaches `maxAttempts`, it is dropped and `onFailed`
|
||||
fires instead of retrying:
|
||||
|
||||
```ts
|
||||
const queue = createQueue({
|
||||
onFailed: (job, error) => {
|
||||
console.error(`job ${job.id} (${job.name}) gave up`, error);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Deterministic testing
|
||||
|
||||
Instead of `start()`, inject a clock and drive the queue with `drain()`:
|
||||
|
||||
```ts
|
||||
let clock = 0;
|
||||
const queue = createQueue({ now: () => clock });
|
||||
|
||||
queue.process("task", async () => {
|
||||
/* ... */
|
||||
});
|
||||
await queue.add("task", {}, { delayMs: 5000 });
|
||||
|
||||
clock = 5000;
|
||||
const ran = await queue.drain(); // => 1
|
||||
```
|
||||
|
||||
### Durable workflows and approvals
|
||||
|
||||
`createWorkflowEngine(store)` executes dependency-ordered steps and persists every transition,
|
||||
result, progress update, failure, cancellation, and approval record. Approval steps pause safely
|
||||
and can resume after a process restart because the snapshot lives in the supplied `WorkflowStore`.
|
||||
|
||||
```ts
|
||||
const workflow = defineDurableWorkflow({
|
||||
name: "publish-report",
|
||||
steps: [
|
||||
{ name: "build", run: buildReport },
|
||||
{ name: "approve", dependsOn: ["build"], approval: true, run: (report) => report },
|
||||
{ name: "publish", dependsOn: ["approve"], run: publishReport },
|
||||
],
|
||||
});
|
||||
|
||||
const run = await engine.start(workflow, input);
|
||||
await engine.approve(workflow, run.id, "approve", currentUser.id);
|
||||
```
|
||||
|
||||
Use `memoryWorkflowStore()` for tests. Production stores implement the small `get`, `put`, and
|
||||
`list` contract using the same transactional database or durable service as the application.
|
||||
|
||||
## Retry & backoff behavior
|
||||
|
||||
- On a thrown handler error, the job is retried while `attempts < maxAttempts`.
|
||||
- The next `runAt` is set to `now + backoffMs * 2^(attempts - 1)` (exponential
|
||||
backoff): with `backoffMs: 1000` the delays are 1s, 2s, 4s, …
|
||||
- A job whose worker name has no registered handler stays queued until one is
|
||||
registered (it is not counted as runnable by `drain`).
|
||||
- `drain` is re-entrant-safe: overlapping calls are skipped while one is running.
|
||||
|
||||
## Requirements / Notes
|
||||
|
||||
- **Bun-only** runtime (Node is not supported), consistent with the rest of the
|
||||
WrNexus framework. The queue itself relies only on standard timers
|
||||
(`setInterval`/`clearInterval`) and has no runtime dependencies.
|
||||
- The default store is in-process, so queued jobs do not survive a restart; a
|
||||
pluggable driver is intended for backing it with Redis/SQL for durability.
|
||||
- Works alongside `@wrnexus/core` for offloading work from the request path.
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/queue"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/queue",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"queue"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
# @wrnexus/reactive
|
||||
|
||||
> Tiny, type-safe reactive primitives (signals) with zero dependencies.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
## Overview
|
||||
|
||||
`@wrnexus/reactive` is the seed of WrNexus's reactivity layer: a minimal `signal`
|
||||
primitive that holds a value, notifies subscribers when it changes, and hands back
|
||||
an unsubscribe function. It is deliberately small and framework-agnostic — it powers
|
||||
nothing on its own, but is shaped so client islands (and later the `.wrn` compiler's
|
||||
`state` blocks) can build reactive bindings on top of it. Reach for it when you need
|
||||
observable state without pulling in a full reactivity library.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @wrnexus/reactive
|
||||
```
|
||||
|
||||
> Private package — the machine must be authenticated to the `wrnexus` npm org
|
||||
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
|
||||
|
||||
## API
|
||||
|
||||
The package has a single entry point (`.`) exporting one function and three types.
|
||||
|
||||
### `signal<T>(initial: T): Signal<T>`
|
||||
|
||||
Creates a reactive signal seeded with `initial`. Returns a `Signal<T>`:
|
||||
|
||||
| Member | Signature | Description |
|
||||
| ----------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `get` | `(): T` | Read the current value. |
|
||||
| `set` | `(next: T): void` | Write a new value. Subscribers run **only when the value actually changes** (compared with `Object.is`). |
|
||||
| `update` | `(fn: (current: T) => T): void` | Apply a function to the current value; equivalent to `set(fn(get()))`. |
|
||||
| `subscribe` | `(fn: Subscriber<T>): Unsubscribe` | Register a subscriber; returns a function that removes it. |
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
type Subscriber<T> = (value: T) => void;
|
||||
type Unsubscribe = () => void;
|
||||
|
||||
interface Signal<T> {
|
||||
get(): T;
|
||||
set(next: T): void;
|
||||
update(fn: (current: T) => T): void;
|
||||
subscribe(fn: Subscriber<T>): Unsubscribe;
|
||||
}
|
||||
```
|
||||
|
||||
Notes on semantics:
|
||||
|
||||
- **No-op updates are skipped.** `set` compares the incoming value to the current
|
||||
one with `Object.is`; identical values do not notify subscribers.
|
||||
- **Safe unsubscribe during notification.** Subscribers are iterated over a copy of
|
||||
the subscriber set, so a subscriber may call its own (or another's) unsubscribe
|
||||
while a notification is in flight.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { signal } from "@wrnexus/reactive";
|
||||
|
||||
const count = signal(0);
|
||||
|
||||
count.get(); // 0
|
||||
|
||||
// Subscribe; the returned function unsubscribes.
|
||||
const off = count.subscribe((value) => {
|
||||
console.log("count is now", value);
|
||||
});
|
||||
|
||||
count.set(1); // logs: count is now 1
|
||||
count.set(1); // no-op — value unchanged, no notification
|
||||
count.update((n) => n + 1); // logs: count is now 2
|
||||
|
||||
off(); // stop listening
|
||||
count.set(3); // nothing logged
|
||||
```
|
||||
|
||||
Typed signals infer `T` from the initial value, or can be annotated explicitly:
|
||||
|
||||
```ts
|
||||
import { signal, type Signal } from "@wrnexus/reactive";
|
||||
|
||||
const user: Signal<{ name: string } | null> = signal(null);
|
||||
user.set({ name: "Ada" });
|
||||
```
|
||||
|
||||
## Requirements / Notes
|
||||
|
||||
- **Bun-only.** Distributed as TypeScript source (`main`/`exports` point at
|
||||
`src/index.ts`); consume it under Bun, which runs `.ts` directly.
|
||||
- **Zero dependencies.** The only runtime API used is the standard `Object.is`.
|
||||
- Foundational primitive for WrNexus client islands and the forthcoming `.wrn`
|
||||
compiler `state` blocks.
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/reactive"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/reactive",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"reactive"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -37,8 +37,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/compiler": "^0.8.5",
|
||||
"@wrnexus/core": "^0.8.5"
|
||||
"@wrnexus/compiler": "^0.8.9",
|
||||
"@wrnexus/core": "^0.8.9"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -89,7 +89,7 @@ const html = renderDocument({
|
||||
},
|
||||
url: new URL("https://acme.example/about"),
|
||||
body: "<h1>About Us</h1>",
|
||||
scripts: ["/_wire/runtime.js", "/_wire/islands/about.js"],
|
||||
scripts: ["/_wrn/runtime.js", "/_wrn/islands/about.js"],
|
||||
htmlAttrs: ' data-theme="dark"',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -45,9 +45,9 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.8.5",
|
||||
"@wrnexus/store": "^0.8.5",
|
||||
"@wrnexus/security": "^0.8.5"
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/store": "^0.8.8",
|
||||
"@wrnexus/security": "^0.8.8"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
+13
-13
@@ -21,7 +21,7 @@ other arrays intentionally replace earlier values. Cycles and missing/invalid
|
||||
entries fail with stable `WRN-CONFIG-LAYER-*` diagnostics. `wrnexus config
|
||||
--explain` lists every resolved layer source.
|
||||
|
||||
> Global CSS bundling, the `--wire-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps.
|
||||
> Global CSS bundling, the `--wrn-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
@@ -30,7 +30,7 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
|
||||
This package owns three server-side concerns that shape every page a WrNexus app renders:
|
||||
|
||||
1. **Global stylesheet pipeline** — finds `app/styles/global.css` (or aggregates `app/styles/*.css`), bundles it with Bun's CSS bundler (which resolves `@import`, including from `node_modules`), and produces one stylesheet that is `<link>`ed into every page's `<head>`. Because it is a plain global sheet, it styles server-rendered markup and hydrated client islands identically. A custom `process` hook lets you swap in Tailwind / PostCSS / Sass.
|
||||
2. **Theme system** — design tokens exposed as CSS custom properties (`--wire-<key>`), with built-in `light`/`dark` sets, deep-merged user overrides, an SSR `<html data-theme>` render (no flash), and a tiny client runtime to toggle/persist the choice.
|
||||
2. **Theme system** — design tokens exposed as CSS custom properties (`--wrn-<key>`), with built-in `light`/`dark` sets, deep-merged user overrides, an SSR `<html data-theme>` render (no flash), and a tiny client runtime to toggle/persist the choice.
|
||||
3. **App config** — loads `wrnexus.config.ts` (the `AppConfig` type), applies named profile overrides, and loads the `.env` cascade.
|
||||
|
||||
It runs server-side / at build time. Reach for it when configuring an app, defining themes, or customising how global CSS is produced.
|
||||
@@ -111,7 +111,7 @@ interface StyleProcessContext {
|
||||
| Export | Type / Signature | Purpose |
|
||||
| -------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `DEFAULT_THEMES` | `Record<string, ThemeTokens>` | Built-in `light` and `dark` token maps. |
|
||||
| `THEME_COOKIE` | `"wire-theme"` | Cookie the resolved theme is read from / persisted to. |
|
||||
| `THEME_COOKIE` | `"wrn-theme"` | Cookie the resolved theme is read from / persisted to. |
|
||||
| `THEME_CSS_HREF` | `"/__wrnexus/theme.css"` | URL the generated theme stylesheet is served at. |
|
||||
| `THEME_JS_HREF` | `"/__wrnexus/theme.js"` | URL the client theme runtime is served at. |
|
||||
| `resolveThemeConfig` | `(config?: ThemeConfig) => ResolvedTheme` | Deep-merge the user's `theme` config over the defaults; pick the default theme (config's `default` if valid, else `dark`, else the first). |
|
||||
@@ -119,7 +119,7 @@ interface StyleProcessContext {
|
||||
| `renderThemeCss` | `(theme: ResolvedTheme) => string` | Generate the theme stylesheet: a `:root{…}` default plus one `[data-theme="<name>"]{…}` block per theme. |
|
||||
| `renderThemeRuntime` | `(theme: ResolvedTheme) => string` | Generate the client runtime (see below). |
|
||||
|
||||
Tokens are emitted as `--wire-<key>` custom properties, **except** the reserved key `color-scheme`, which is emitted as the native `color-scheme` CSS property so form controls and scrollbars match the theme.
|
||||
Tokens are emitted as `--wrn-<key>` custom properties, **except** the reserved key `color-scheme`, which is emitted as the native `color-scheme` CSS property so form controls and scrollbars match the theme.
|
||||
|
||||
`ThemeConfig` / `ThemeTokens` / `ResolvedTheme`:
|
||||
|
||||
@@ -164,7 +164,7 @@ theme: {
|
||||
}
|
||||
```
|
||||
|
||||
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.wrnTheme` with `{ get, set, toggle, bind, themes }`, connects up any `[data-wrn-theme-toggle]` and `[data-wrn-theme-set]` elements on load, and persists the choice to the `wrn-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -252,17 +252,17 @@ In templates, consume tokens via the custom properties:
|
||||
|
||||
```css
|
||||
.card {
|
||||
background: var(--wire-color-surface);
|
||||
color: var(--wire-color-text);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: var(--wire-radius);
|
||||
box-shadow: var(--wire-shadow-1);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius);
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<button data-wire-theme-toggle>Toggle theme</button>
|
||||
<button data-wire-theme-set="brand">Brand theme</button>
|
||||
<button data-wrn-theme-toggle>Toggle theme</button>
|
||||
<button data-wrn-theme-set="brand">Brand theme</button>
|
||||
```
|
||||
|
||||
## Requirements / Notes
|
||||
@@ -270,4 +270,4 @@ In templates, consume tokens via the custom properties:
|
||||
- **Bun-only.** `bundleCss` uses `Bun.build`'s CSS bundler for `@import` resolution, nesting, and minification. Node is not supported.
|
||||
- Config and env loading use `node:fs` / `node:path` / `node:url` and read from `process.env`.
|
||||
- Peer package: `@wrnexus/core` supplies the `SeoConfig` and `SecurityConfig` types referenced by `AppConfig`.
|
||||
- The bundled global stylesheet, the theme stylesheet (`THEME_CSS_HREF`), and the theme runtime (`THEME_JS_HREF`) are wired into pages by the framework's server; this package only produces their contents.
|
||||
- The bundled global stylesheet, the theme stylesheet (`THEME_CSS_HREF`), and the theme runtime (`THEME_JS_HREF`) are connected into pages by the framework's server; this package only produces their contents.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.10",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -37,9 +37,9 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/uploader": "^0.8.5",
|
||||
"@wrnexus/core": "^0.8.5",
|
||||
"@wrnexus/plugin": "^0.8.5"
|
||||
"@wrnexus/uploader": "^0.8.9",
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/plugin": "^0.8.8"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# @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.
|
||||
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/syntax",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/syntax — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/syntax"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/syntax",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"syntax"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
},
|
||||
"./formatter": {
|
||||
"types": "./dist/formatter.d.ts",
|
||||
"import": "./dist/formatter.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# @wrnexus/tracking
|
||||
|
||||
> Error tracking for WrNexus apps: capture exceptions manually or via middleware and fan them out to pluggable sinks.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
## Overview
|
||||
|
||||
`@wrnexus/tracking` is a small, server-side error-capture layer. You create a
|
||||
tracker with one or more **sinks**, then feed it errors — either manually with
|
||||
`tracker.capture(err, context)` or automatically by mounting `tracker.middleware()`
|
||||
in your request pipeline. A `consoleSink` is included; forwarding to Sentry,
|
||||
Datadog, or any other backend is just a matter of writing a tiny sink. Reach for
|
||||
it when you want a single, sink-agnostic place to route application errors. Sinks
|
||||
run best-effort — a throwing sink never breaks the request.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @wrnexus/tracking
|
||||
```
|
||||
|
||||
> Private package — the machine must be authenticated to the `wrnexus` npm org
|
||||
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
|
||||
|
||||
## API
|
||||
|
||||
### `createTracker(options?): Tracker`
|
||||
|
||||
Creates a tracker. `TrackerOptions`:
|
||||
|
||||
| Option | Type | Description |
|
||||
| ------------ | ------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `sinks` | `ErrorSink[]` | Initial sinks to fan events out to. Defaults to `[]`. |
|
||||
| `now` | `() => number` | Clock used for `event.timestamp` (epoch ms). Defaults to `Date.now`. |
|
||||
| `beforeSend` | `(event: ErrorEvent) => ErrorEvent \| null` | Scrub/enrich an event before it reaches any sink. Return `null` to drop it. |
|
||||
|
||||
The returned `Tracker`:
|
||||
|
||||
| Member | Signature | Description |
|
||||
| ------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `capture` | `(error: unknown, context?: Record<string, unknown>) => Promise<void>` | Normalizes any thrown value into an `Error`, builds an `ErrorEvent`, runs `beforeSend`, then dispatches to all sinks. Non-`Error` values are wrapped in an `Error` named `NonError`. |
|
||||
| `addSink` | `(sink: ErrorSink) => void` | Registers an additional sink at runtime. |
|
||||
| `middleware` | `() => Middleware` | Returns a WrNexus `Middleware` that captures any error thrown downstream, then re-throws it so the framework's error handler still produces the response. |
|
||||
|
||||
The middleware attaches this context to captured events:
|
||||
|
||||
```ts
|
||||
{ method: ctx.req.method, path: ctx.url.pathname, requestId: ctx.locals.requestId }
|
||||
```
|
||||
|
||||
### `consoleSink: ErrorSink`
|
||||
|
||||
A built-in sink that logs a compact one-line message via `console.error`, e.g.
|
||||
`[error] TypeError: cannot read x {"userId":42}`.
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
interface ErrorEvent {
|
||||
error: Error;
|
||||
context: Record<string, unknown>; // request info, user id, tags…
|
||||
timestamp: number; // epoch ms
|
||||
}
|
||||
|
||||
interface ErrorSink {
|
||||
name?: string;
|
||||
capture(event: ErrorEvent): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Manual capture:
|
||||
|
||||
```ts
|
||||
import { createTracker, consoleSink } from "@wrnexus/tracking";
|
||||
|
||||
const tracker = createTracker({ sinks: [consoleSink] });
|
||||
|
||||
try {
|
||||
await doWork();
|
||||
} catch (err) {
|
||||
await tracker.capture(err, { userId: 42, op: "doWork" });
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
As request middleware:
|
||||
|
||||
```ts
|
||||
import { createTracker, consoleSink } from "@wrnexus/tracking";
|
||||
|
||||
const tracker = createTracker({ sinks: [consoleSink] });
|
||||
|
||||
app.use(tracker.middleware()); // captures + re-throws downstream errors
|
||||
```
|
||||
|
||||
A custom sink with `beforeSend` scrubbing:
|
||||
|
||||
```ts
|
||||
import { createTracker, type ErrorSink } from "@wrnexus/tracking";
|
||||
|
||||
const sentrySink: ErrorSink = {
|
||||
name: "sentry",
|
||||
async capture(event) {
|
||||
await Sentry.captureException(event.error, { extra: event.context });
|
||||
},
|
||||
};
|
||||
|
||||
const tracker = createTracker({
|
||||
sinks: [sentrySink],
|
||||
beforeSend(event) {
|
||||
delete event.context.password; // scrub secrets
|
||||
return event; // return null to drop the event entirely
|
||||
},
|
||||
});
|
||||
|
||||
tracker.addSink(anotherSink); // add more sinks later
|
||||
```
|
||||
|
||||
## Requirements / Notes
|
||||
|
||||
- Runs on **Bun** only (Node is not supported).
|
||||
- Peer package: [`@wrnexus/core`](../core) — the `Context` and `Middleware` types
|
||||
used by `tracker.middleware()` come from there.
|
||||
- Sink dispatch is fire-and-forget-safe: all sinks run via `Promise.all`, and a
|
||||
sink that throws is swallowed so it can never break the app.
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.8.5",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
|
||||
"directory": "packages/tracking"
|
||||
},
|
||||
"homepage": "https://wrnexusjs.dev/packages/tracking",
|
||||
"bugs": {
|
||||
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"wrnexus",
|
||||
"bun",
|
||||
"typescript",
|
||||
"tracking"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
+42
-96
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
This reference is generated from the 102 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
|
||||
|
||||
## Advanced-forms
|
||||
|
||||
@@ -22,15 +22,6 @@ Editable autocomplete combobox with local and remote suggestions.
|
||||
- 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.
|
||||
@@ -229,7 +220,7 @@ Present structured responsive linked or status items with icons, descriptions, a
|
||||
- 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
|
||||
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||
|
||||
### ListGroup
|
||||
|
||||
@@ -247,7 +238,7 @@ Continuously present responsive labels, partners, notices, or capabilities with
|
||||
- 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
|
||||
- Outputs: `pause({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `resume({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||
|
||||
### Progress
|
||||
|
||||
@@ -301,25 +292,16 @@ Present responsive chronological activity, milestones, or workflow status with r
|
||||
- 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 })`
|
||||
- Outputs: `select({ 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 = ""`
|
||||
- Props: `title: string = "Tree View"`, `description: string = ""`, `items: unknown[] = []`, `valueKey: string = "value"`, `labelKey: string = "label"`, `defaultExpanded: unknown[] = []`, `selected: string = ""`, `size: string = "default"`, `color: string = "primary"`, `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 })`
|
||||
- Outputs: `select({ item: object; value: string; sourceEvent?: Event })`, `toggle({ item: object; value: string; expanded: boolean; sourceEvent?: Event })`, `expand({ item: object; value: string; sourceEvent?: Event })`, `collapse({ item: object; value: string; sourceEvent?: Event })`
|
||||
|
||||
## Core
|
||||
|
||||
@@ -469,7 +451,7 @@ Provide an accessible responsive search field with labels, validation states, si
|
||||
- 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
|
||||
- Outputs: `search({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||
|
||||
### Select
|
||||
|
||||
@@ -509,50 +491,32 @@ Theme-aware, responsive time picker component.
|
||||
|
||||
## 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 = ""`
|
||||
- Props: `title: string = "Chart"`, `description: string = ""`, `items: unknown[] = []`, `valueKey: string = "value"`, `labelKey: string = "label"`, `height: number = 240`, `showLegend: boolean = true`, `showValues: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "bar"`, `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[])`
|
||||
- Outputs: `select({ item: object; index: number; sourceEvent?: Event })`, `dataPointClick({ item: object; index: number; sourceEvent?: Event })`, `legendToggle({ item: object; index: number; hidden: boolean; sourceEvent?: Event })`
|
||||
|
||||
### 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 = ""`
|
||||
- Props: `value: string = ""`, `label: string = "Copy"`, `copiedLabel: string = "Copied"`, `errorLabel: string = "Copy failed"`, `title: string = "Clipboard"`, `description: string = ""`, `code: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `disabled: boolean = false`, `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)`
|
||||
- Outputs: `copy({ value: string; sourceEvent?: Event })`, `success({ value: string; sourceEvent?: Event })`, `error({ error: Error | string; value: string; sourceEvent?: Event })`
|
||||
|
||||
### 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 = ""`
|
||||
- Props: `label: string = "Celebrate"`, `duration: number = 1200`, `count: number = 24`, `autoStart: boolean = false`, `disabled: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `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 })`
|
||||
- Outputs: `start({ sourceEvent?: Event; duration: number })`, `complete({ sourceEvent?: Event; duration: number })`
|
||||
|
||||
### DataMap
|
||||
|
||||
@@ -572,15 +536,6 @@ Theme-aware, responsive drag and drop component.
|
||||
- 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.
|
||||
@@ -588,16 +543,7 @@ Present responsive location information and markers with map-ready metadata and
|
||||
- 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 })`
|
||||
- Outputs: `markerClick({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `zoom({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||
|
||||
### WysiwygEditor
|
||||
|
||||
@@ -624,7 +570,7 @@ Create responsive balanced content columns with configurable count, gap, density
|
||||
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 = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `centered: boolean = true`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: None
|
||||
|
||||
@@ -633,16 +579,16 @@ Constrain and align page content with responsive gutters and compact, wide, or f
|
||||
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 = ""`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `axis: string = "vertical"`, `thickness: number = 8`, `maxHeight: string = "20rem"`, `radius: string = "999px"`, `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[])`
|
||||
- Outputs: None
|
||||
|
||||
### 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 = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `variant: string = "solid"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Outputs: None
|
||||
|
||||
@@ -669,7 +615,7 @@ Render structured responsive footer navigation, pre and post content, copyright
|
||||
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 = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `minItemWidth: string = ""`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: None
|
||||
|
||||
@@ -678,7 +624,7 @@ Arrange arbitrary content in a responsive configurable CSS grid with stable colu
|
||||
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 = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `fit: string = "cover"`, `rounded: boolean = false`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: None
|
||||
|
||||
@@ -687,7 +633,7 @@ Render a responsive image with explicit dimensions, loading behavior, alternativ
|
||||
Theme-aware, responsive kbd component.
|
||||
|
||||
- Mount: `data-component="Kbd"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "⌘ K"`, `class: string = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: None
|
||||
|
||||
@@ -696,16 +642,16 @@ Theme-aware, responsive kbd component.
|
||||
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[])`
|
||||
- Props: `color: string = "primary"`, `size: number = 50`, `orientation: string = "horizontal"`, `minSize: number = 15`, `step: number = 5`, `label: string = "Resize panels"`, `class: string = ""`
|
||||
- Slots: `start`, `end`, `default`
|
||||
- Outputs: `sizeChange({ size: number })`
|
||||
|
||||
### 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 = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `underline: string = "hover"`, `external: boolean = false`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: None
|
||||
|
||||
@@ -862,18 +808,18 @@ Show responsive hierarchical navigation with home support, separators, current-p
|
||||
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 = ""`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `footer: string = ""`, `defaultOpen: boolean = false`, `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)`
|
||||
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
|
||||
|
||||
### 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 = ""`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `label: string = "Main"`, `collapsible: boolean = true`, `toggleLabel: string = "Menu"`, `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)`
|
||||
- Outputs: `select({ item: object; value: string })`
|
||||
|
||||
### Navbar
|
||||
|
||||
@@ -889,45 +835,45 @@ Theme-aware, responsive navbar component.
|
||||
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 = ""`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `page: number = 1`, `pageSize: number = 10`, `total: number = 0`, `variant: string = "compact"`, `siblingCount: number = 1`, `showSummary: boolean = true`, `label: string = "Pagination"`, `previousLabel: string = "Previous"`, `nextLabel: string = "Next"`, `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 })`
|
||||
- Outputs: `change({ page: number; pageSize: number })`, `previous({ page: number })`, `next({ page: number })`
|
||||
|
||||
### 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 = ""`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `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)`
|
||||
- Outputs: `change({ href: string; label: string })`
|
||||
|
||||
### 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 })`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `mobileLabel: string = "Open navigation"`, `drawerTitle: string = "Navigation"`, `class: string = ""`
|
||||
- Slots: `default`, `drawer`
|
||||
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent: Event })`, `close({ source: string })`, `select({ item: object; value: string; level: number })`
|
||||
|
||||
### 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 })`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `steps: unknown[] = []`, `active: number = 0`, `orientation: string = "horizontal"`, `clickable: boolean = false`, `label: string = "Progress"`, `showPanel: boolean = false`, `controls: boolean = false`, `allowSkip: boolean = false`, `nextDisabled: boolean = false`, `backLabel: string = "Back"`, `nextLabel: string = "Next"`, `skipLabel: string = "Skip"`, `finishLabel: string = "Finish"`, `class: string = ""`
|
||||
- Slots: `step-{index}`, `panel-{index}`, `default`
|
||||
- Outputs: `change({ index: number; step: object })`, `back({ index: number; step: object })`, `next({ index: number; step: object })`, `skip({ index: number; step: object })`, `finish({ index: number; step: 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
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mode: string = "client"`, `param: string = "tab"`, `label: string = "Tabs"`, `class: string = ""`
|
||||
- Slots: `panel-{valueOf(item, index)}`, `default`
|
||||
- Outputs: `change({ value: string; item: object; index: number })`, `select({ value: string; item: object; index: number })`
|
||||
|
||||
## Overlays
|
||||
|
||||
|
||||
+40
-44
@@ -1,6 +1,6 @@
|
||||
# @wrnexus/ui
|
||||
|
||||
> First-party Wire UI component library — a set of themeable `.wrn` components plus a single tokenized stylesheet.
|
||||
> First-party WrNexus UI component library — a set of themeable `.wrn` components plus a single tokenized stylesheet.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
@@ -11,7 +11,7 @@ controls, and feedback UI) together with one themeable stylesheet, `ui.css`. The
|
||||
components are **auto-discovered** by the framework router — you don't import them
|
||||
in code. Once the package's component directory is on the router's scan path, you
|
||||
mount any component in a page with `data-component="<name>"`. Every visual is
|
||||
driven by `var(--wire-*)` theme tokens, so components restyle instantly when the
|
||||
driven by `var(--wrn-*)` theme tokens, so components restyle instantly when the
|
||||
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.
|
||||
|
||||
@@ -30,7 +30,7 @@ bun add @wrnexus/ui
|
||||
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
|
||||
|
||||
In practice you rarely install this directly: `@wrnexus/cli` and
|
||||
`@wrnexus/dev-server` already depend on it and wire it into the router for you
|
||||
`@wrnexus/dev-server` already depend on it and wrn it into the router for you
|
||||
(see [Auto-discovery](#auto-discovery)).
|
||||
|
||||
## Components
|
||||
@@ -54,19 +54,19 @@ element), and most render their body from either a named prop or the default slo
|
||||
|
||||
### Core / feedback
|
||||
|
||||
| Name | Purpose | Key props |
|
||||
| -------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `button` | Button | `label`, `variant` (`default`\|`primary`\|`danger`\|`ghost`), `size` (`sm`\|`md`\|`lg`), `type` |
|
||||
| `input` | Text input | see source |
|
||||
| `textarea` | Multi-line input | see source |
|
||||
| `checkbox` | Checkbox | see source |
|
||||
| `badge` | Small status badge | `label`, `variant` |
|
||||
| `alert` | Callout box | `variant` (`info`\|`success`\|`danger`\|`warning`), `title`, `message` |
|
||||
| `card` | Padded, bordered surface | `class` |
|
||||
| `avatar` | User avatar | see source |
|
||||
| `spinner` | Loading indicator | see source |
|
||||
| `disclosure` | Expandable details/summary | see source |
|
||||
| `theme-toggle` | Theme switch button (binds `data-wire-theme-toggle`) | `label` |
|
||||
| Name | Purpose | Key props |
|
||||
| -------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `button` | Button | `label`, `variant` (`default`\|`primary`\|`danger`\|`ghost`), `size` (`sm`\|`md`\|`lg`), `type` |
|
||||
| `input` | Text input | see source |
|
||||
| `textarea` | Multi-line input | see source |
|
||||
| `checkbox` | Checkbox | see source |
|
||||
| `badge` | Small status badge | `label`, `variant` |
|
||||
| `alert` | Callout box | `variant` (`info`\|`success`\|`danger`\|`warning`), `title`, `message` |
|
||||
| `card` | Padded, bordered surface | `class` |
|
||||
| `avatar` | User avatar | see source |
|
||||
| `spinner` | Loading indicator | see source |
|
||||
| `disclosure` | Expandable details/summary | see source |
|
||||
| `theme-toggle` | Theme switch button (binds `data-wrn-theme-toggle`) | `label` |
|
||||
|
||||
### Additional controls & data display
|
||||
|
||||
@@ -100,7 +100,7 @@ are `.wrn` files rendered server-side.
|
||||
| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). |
|
||||
| `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 `.wrn-*` classes, themed via tokens). |
|
||||
| `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. |
|
||||
|
||||
@@ -118,21 +118,17 @@ are `.wrn` files rendered server-side.
|
||||
The framework serves this stylesheet once at `/__wrnexus/ui.css`, so pages get all
|
||||
component styles from a single request.
|
||||
|
||||
### Tailwind and motion
|
||||
### Component styles 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";
|
||||
```
|
||||
Components own their BEM styles in local `style {}` blocks and do not require a
|
||||
Tailwind scan. `ui.css` supplies only global tokens, resets, and shared
|
||||
primitives. Applications can still use Tailwind independently in their own
|
||||
source files.
|
||||
|
||||
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
|
||||
entry and interaction motion. Override `--wrn-motion-fast`,
|
||||
`--wrn-motion-base`, `--wrn-motion-slow`, `--wrn-ease-standard`, or
|
||||
`--wrn-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
|
||||
@@ -143,21 +139,21 @@ 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);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.account-card__action {
|
||||
background: var(--wire-color-primary);
|
||||
color: var(--wire-color-primary-contrast);
|
||||
background: var(--wrn-color-primary);
|
||||
color: var(--wrn-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`.
|
||||
Stable no-spacing helper classes are also available: `wrn-bg-page`,
|
||||
`wrn-bg-surface`, `wrn-bg-surface-2`, `wrn-bg-primary`, `wrn-bg-secondary`,
|
||||
`wrn-text`, `wrn-text-muted`, `wrn-text-primary`, `wrn-text-success`,
|
||||
`wrn-text-warning`, `wrn-text-danger`, and `wrn-border`.
|
||||
|
||||
Tailwind-authored custom markup can continue using the palette families already
|
||||
used by packaged components. `indigo-*` and `violet-*` resolve to primary,
|
||||
@@ -173,7 +169,7 @@ The router scans extra `componentDirs` (in addition to the app's own
|
||||
`app/components`) and keys components by name. Library dirs are scanned **first**
|
||||
and `app/components` **last**, so an app component of the same name shadows the
|
||||
library's. The CLI build (`@wrnexus/cli`) and dev server (`@wrnexus/dev-server`)
|
||||
both wire the UI directory in for you:
|
||||
both wrn the UI directory in for you:
|
||||
|
||||
```ts
|
||||
import { buildRouter } from "@wrnexus/router";
|
||||
@@ -199,11 +195,11 @@ attributes (other than `data-component`) become string props:
|
||||
|
||||
Ways to customize the components, in increasing order of power:
|
||||
|
||||
1. **Theme tokens** — override CSS custom properties such as `--wire-color-primary`,
|
||||
`--wire-color-surface`, `--wire-radius-sm`, etc. Every component style resolves
|
||||
through `var(--wire-*)`, so changing a token restyles everything instantly
|
||||
1. **Theme tokens** — override CSS custom properties such as `--wrn-color-primary`,
|
||||
`--wrn-color-surface`, `--wrn-radius-sm`, etc. Every component style resolves
|
||||
through `var(--wrn-*)`, so changing a token restyles everything instantly
|
||||
(including across theme switches).
|
||||
2. **App CSS** — redefine a `.wire-*` class in your own stylesheet, which is loaded
|
||||
2. **App CSS** — redefine a `.wrn-*` class in your own stylesheet, which is loaded
|
||||
after `ui.css` and therefore wins.
|
||||
3. **`class` prop** — pass a `class` prop to a component; it is appended to the
|
||||
component's root element, letting you add per-instance classes without touching
|
||||
@@ -221,4 +217,4 @@ Ways to customize the components, in increasing order of power:
|
||||
[`@wrnexus/dev-server`](../dev-server) / built by [`@wrnexus/cli`](../cli).
|
||||
- Depends on [`@wrnexus/core`](../core) (`dependencies`).
|
||||
- `theme-toggle` relies on the framework's theme runtime, which binds the
|
||||
`data-wire-theme-toggle` attribute — no per-component JS is required.
|
||||
`data-wrn-theme-toggle` attribute — no per-component JS is required.
|
||||
|
||||
@@ -6,16 +6,6 @@
|
||||
"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",
|
||||
@@ -151,11 +141,6 @@
|
||||
"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",
|
||||
@@ -221,11 +206,6 @@
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive file input component."
|
||||
},
|
||||
{
|
||||
"name": "FileUpload",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive file upload component."
|
||||
},
|
||||
{
|
||||
"name": "FileUploadProgress",
|
||||
"category": "base",
|
||||
@@ -496,16 +476,6 @@
|
||||
"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": "Toaster",
|
||||
"category": "core",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"generatedFrom": "full component catalog reset",
|
||||
"removedCount": 0,
|
||||
"replacements": {}
|
||||
"generatedFrom": "framework remediation 0.8.7",
|
||||
"removedCount": 6,
|
||||
"replacements": {
|
||||
"AdvancedDatePicker": "DatePicker",
|
||||
"AdvancedRangeSlider": "RangeSlider",
|
||||
"FileUpload": "FileInput",
|
||||
"Toast": "Toaster",
|
||||
"ToastNotifications": "Toaster",
|
||||
"CopyMarkup": "Clipboard"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,22 +49,24 @@ component Accordion {
|
||||
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, {
|
||||
// Named rather than computed: an output is resolved as a property name, so
|
||||
// output[eventName] would not reach a parent binding.
|
||||
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, payload) {
|
||||
payload = {
|
||||
component: "Accordion",
|
||||
value: value,
|
||||
item: item,
|
||||
open: isOpen(value),
|
||||
openValues: openValues
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
}
|
||||
|
||||
if (eventName === "open") {
|
||||
output.open(payload)
|
||||
} else if (eventName === "close") {
|
||||
output.close(payload)
|
||||
} else {
|
||||
output.change(payload)
|
||||
}
|
||||
}
|
||||
|
||||
client function toggleItem(sourceEvent, value, item, wasOpen) {
|
||||
@@ -101,15 +103,15 @@ component 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}"
|
||||
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--accordion wrn-next--accordion-{variant} {bordered ? 'wrn-next--accordion-bordered' : ''} {separated ? 'wrn-next--accordion-separated' : ''} {flush ? 'wrn-next--accordion-flush' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
|
||||
>
|
||||
{#each items as item, index}
|
||||
<section
|
||||
class="wire-next__accordion-item"
|
||||
class="wrn-next__accordion-item"
|
||||
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
|
||||
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
|
||||
>
|
||||
<h3 class="wire-next__accordion-heading">
|
||||
<h3 class="wrn-next__accordion-heading">
|
||||
<button
|
||||
type="button"
|
||||
id="{id}-trigger-{index}"
|
||||
@@ -119,7 +121,7 @@ component Accordion {
|
||||
@click="toggleItem(event, itemValue(item, index), item)"
|
||||
>
|
||||
{#if showIndicator && indicatorPosition === "start"}
|
||||
<span class="wire-next__accordion-indicator" aria-hidden="true">
|
||||
<span class="wrn-next__accordion-indicator" aria-hidden="true">
|
||||
{#if indicator === "chevron"}
|
||||
<span class="icon-[lucide--chevron-down]"></span>
|
||||
{:else}
|
||||
@@ -129,7 +131,7 @@ component Accordion {
|
||||
{/if}
|
||||
<span>{item.label || item.title}</span>
|
||||
{#if showIndicator && indicatorPosition === "end"}
|
||||
<span class="wire-next__accordion-indicator" aria-hidden="true">
|
||||
<span class="wrn-next__accordion-indicator" aria-hidden="true">
|
||||
{#if indicator === "chevron"}
|
||||
<span class="icon-[lucide--chevron-down]"></span>
|
||||
{:else}
|
||||
@@ -142,23 +144,23 @@ component Accordion {
|
||||
|
||||
<div
|
||||
id="{id}-panel-{index}"
|
||||
class="wire-next__accordion-panel"
|
||||
class="wrn-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' : ''}">
|
||||
<div class="wrn-next__accordion-content {contentItalic ? 'wrn-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">
|
||||
<div class="wrn-next__accordion-nested">
|
||||
{#each item.children as child, childIndex}
|
||||
<section
|
||||
class="wire-next__accordion-item"
|
||||
class="wrn-next__accordion-item"
|
||||
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
|
||||
>
|
||||
<h4 class="wire-next__accordion-heading">
|
||||
<h4 class="wrn-next__accordion-heading">
|
||||
<button
|
||||
type="button"
|
||||
id="{id}-trigger-{index}-{childIndex}"
|
||||
@@ -168,7 +170,7 @@ component Accordion {
|
||||
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
|
||||
>
|
||||
{#if showIndicator}
|
||||
<span class="wire-next__accordion-indicator" aria-hidden="true">
|
||||
<span class="wrn-next__accordion-indicator" aria-hidden="true">
|
||||
{#if indicator === "chevron"}
|
||||
<span class="icon-[lucide--chevron-down]"></span>
|
||||
{:else}
|
||||
@@ -181,13 +183,13 @@ component Accordion {
|
||||
</h4>
|
||||
<div
|
||||
id="{id}-panel-{index}-{childIndex}"
|
||||
class="wire-next__accordion-panel"
|
||||
class="wrn-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' : ''}">
|
||||
<div class="wrn-next__accordion-content {contentItalic ? 'wrn-next__accordion-content-italic' : ''}">
|
||||
<p>{child.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,4 +205,274 @@ component Accordion {
|
||||
{/each}
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--accordion {
|
||||
--wrn-accordion-accent: var(--wrn-component-color);
|
||||
display: grid;
|
||||
width: 100%;
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wrn-next__accordion-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-heading {
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-heading > button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-heading > button > span:not(.wrn-next__accordion-indicator) {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-heading > button:focus-visible {
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
outline: 2px solid var(--wrn-accordion-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-heading > button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-indicator {
|
||||
display: inline-flex;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex: 0 0 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
transition:
|
||||
color var(--wrn-motion-fast) var(--wrn-ease-standard),
|
||||
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
|
||||
}
|
||||
|
||||
.wrn-next__accordion-indicator > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-indicator > [class*="icon-["] {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-item[data-open="true"]
|
||||
> .wrn-next__accordion-heading
|
||||
.wrn-next__accordion-indicator {
|
||||
color: var(--wrn-accordion-accent);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.wrn-next--accordion[data-indicator="chevron"]
|
||||
.wrn-next__accordion-item[data-open="true"]
|
||||
> .wrn-next__accordion-heading
|
||||
.wrn-next__accordion-indicator {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.wrn-next__accordion-item[data-open="true"] > .wrn-next__accordion-heading > button {
|
||||
color: var(--wrn-accordion-accent);
|
||||
}
|
||||
|
||||
.wrn-next__accordion-panel {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
transition:
|
||||
grid-template-rows var(--wrn-motion-base) var(--wrn-ease-standard),
|
||||
opacity var(--wrn-motion-fast) var(--wrn-ease-standard);
|
||||
}
|
||||
|
||||
.wrn-next__accordion-item[data-open="true"] > .wrn-next__accordion-panel {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-panel > div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-content {
|
||||
padding: 0 0 1rem 1.75rem;
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-content > p {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-content-italic > p::first-line {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-nested {
|
||||
display: grid;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.wrn-next__accordion-nested .wrn-next__accordion-content {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.wrn-next--accordion-bordered {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
|
||||
.wrn-next--accordion-bordered > .wrn-next__accordion-item + .wrn-next__accordion-item {
|
||||
border-top: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wrn-next--accordion-bordered
|
||||
> .wrn-next__accordion-item
|
||||
> .wrn-next__accordion-heading
|
||||
> button {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next--accordion-bordered
|
||||
> .wrn-next__accordion-item
|
||||
> .wrn-next__accordion-panel
|
||||
.wrn-next__accordion-content {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next--accordion-separated {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wrn-next--accordion-separated > .wrn-next__accordion-item {
|
||||
overflow: hidden;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
|
||||
.wrn-next--accordion-separated > .wrn-next__accordion-item[data-open="true"] {
|
||||
border-color: var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wrn-next--accordion-separated
|
||||
> .wrn-next__accordion-item
|
||||
> .wrn-next__accordion-heading
|
||||
> button,
|
||||
.wrn-next--accordion-separated
|
||||
> .wrn-next__accordion-item
|
||||
> .wrn-next__accordion-panel
|
||||
.wrn-next__accordion-content {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next--accordion-flush {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wrn-next__accordion-panel,
|
||||
.wrn-next__accordion-indicator {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import SelectStyles from "../styles/SelectStyles.wrn"
|
||||
|
||||
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)
|
||||
@@ -253,7 +255,7 @@ component AdvancedSelect {
|
||||
|
||||
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}"
|
||||
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--advanced-select {open ? 'wrn-next--open' : ''} {fixed ? 'wrn-next--advanced-select-fixed' : ''} {invalid ? 'wrn-next--invalid' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
|
||||
data-placement="{placement}"
|
||||
data-search-mode="{searchMode}"
|
||||
data-remote="{remote ? 'true' : 'false'}"
|
||||
@@ -266,17 +268,17 @@ component AdvancedSelect {
|
||||
data-wrn-select
|
||||
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
|
||||
>
|
||||
<div class="wire-next__row">
|
||||
<div class="wrn-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">
|
||||
<div class="wrn-next__select-control">
|
||||
<button
|
||||
{...attrs}
|
||||
id="{name}-trigger"
|
||||
type="button"
|
||||
class="wire-next__select-trigger"
|
||||
class="wrn-next__select-trigger"
|
||||
role="combobox"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="{open}"
|
||||
@@ -287,7 +289,7 @@ component AdvancedSelect {
|
||||
@click="toggle()"
|
||||
@keydown="handleKeydown(event)"
|
||||
>
|
||||
<span class="wire-next__select-value">
|
||||
<span class="wrn-next__select-value">
|
||||
<span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
|
||||
<span
|
||||
data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
|
||||
@@ -298,7 +300,7 @@ component AdvancedSelect {
|
||||
data-text="selectedText()"
|
||||
>{selectedText()}</span>
|
||||
<span
|
||||
class="wire-next__select-tags"
|
||||
class="wrn-next__select-tags"
|
||||
aria-label="{selectedOptionsLabel}"
|
||||
data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
|
||||
>
|
||||
@@ -306,25 +308,25 @@ component AdvancedSelect {
|
||||
<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}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wrn-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 class="wrn-next__placeholder">{placeholder}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="wire-next__select-chevron icon-[lucide--chevrons-up-down]"
|
||||
class="wrn-next__select-chevron icon-[lucide--chevrons-up-down]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__clear-select"
|
||||
class="wrn-next__clear-select"
|
||||
data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
|
||||
aria-label="{clearLabel}"
|
||||
title="{clearLabel}"
|
||||
@@ -335,14 +337,14 @@ component AdvancedSelect {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
|
||||
class="wrn-next__select-dropdown {fixed ? 'wrn-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>
|
||||
<label class="wrn-next__select-search">
|
||||
<span class="wrn-visually-hidden">{searchPlaceholder}</span>
|
||||
<span class="wrn-next__search-icon" aria-hidden="true">⌕</span>
|
||||
<input
|
||||
type="search"
|
||||
value="{query}"
|
||||
@@ -355,28 +357,28 @@ component AdvancedSelect {
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-next__select-message"
|
||||
class="wrn-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>
|
||||
<div class="wrn-next__select-message" data-show="loading" role="status">
|
||||
<i class="wrn-spinner wrn-spinner--inline" aria-hidden="true"></i>
|
||||
{loadingLabel}
|
||||
</div>
|
||||
<div
|
||||
id="{name}-listbox"
|
||||
class="wire-next__select-list"
|
||||
class="wrn-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>
|
||||
<div class="wrn-next__option-group" role="group" aria-label="{group.label}">
|
||||
<div class="wrn-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' : ''}"
|
||||
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
|
||||
data-option-value="{option.value}"
|
||||
data-show="matches(option)"
|
||||
role="option"
|
||||
@@ -387,10 +389,10 @@ component AdvancedSelect {
|
||||
>
|
||||
{#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}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wrn-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]"
|
||||
class="wrn-next__check icon-[lucide--check]"
|
||||
data-show="isSelected(option)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
@@ -403,7 +405,7 @@ component AdvancedSelect {
|
||||
{#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' : ''}"
|
||||
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
|
||||
data-option-value="{option.value}"
|
||||
data-show="matches(option)"
|
||||
role="option"
|
||||
@@ -414,10 +416,10 @@ component AdvancedSelect {
|
||||
>
|
||||
{#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}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wrn-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]"
|
||||
class="wrn-next__check icon-[lucide--check]"
|
||||
data-show="isSelected(option)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
@@ -425,16 +427,16 @@ component AdvancedSelect {
|
||||
{/each}
|
||||
|
||||
{#if !loading && !flatVisibleOptions().length}
|
||||
<div class="wire-next__select-message">{emptyLabel}</div>
|
||||
<div class="wrn-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>
|
||||
<button type="button" class="wrn-next__create-option">{createLabel} “{query}”</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__load-more"
|
||||
class="wrn-next__load-more"
|
||||
data-wrn-select-load-more
|
||||
data-show="infinite && hasMore"
|
||||
>{loadMoreLabel}</button>
|
||||
@@ -451,9 +453,136 @@ component AdvancedSelect {
|
||||
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
|
||||
<small
|
||||
id="{name}-validation"
|
||||
class="wire-next__validation"
|
||||
class="wrn-next__validation"
|
||||
data-error="{name}"
|
||||
>{validationMessage}</small>
|
||||
</div>
|
||||
<SelectStyles hidden />
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next__select-search {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wrn-next__select-search input {
|
||||
padding-left: 2.25em;
|
||||
}
|
||||
|
||||
.wrn-next__select-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.wrn-next__select-tags > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.3rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
color: var(--wrn-component-color);
|
||||
background: color-mix(in srgb, var(--wrn-component-color) 12%, transparent);
|
||||
font-size: 0.75em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wrn-next__select-tags img {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wrn-next__select-value {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.55em;
|
||||
}
|
||||
|
||||
.wrn-next__placeholder {
|
||||
overflow: hidden;
|
||||
color: var(--wrn-color-muted);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,38 +30,34 @@ component AvatarGroup {
|
||||
return items.slice(Number(maxVisible))
|
||||
}
|
||||
|
||||
client function toggleOverflow(sourceEvent, root, customEvent) {
|
||||
client function toggleOverflow() {
|
||||
overflowOpen = !overflowOpen
|
||||
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
|
||||
|
||||
customEvent = document.createEvent("CustomEvent")
|
||||
customEvent.initCustomEvent("overflow", true, false, {
|
||||
output.overflow({
|
||||
component: "AvatarGroup",
|
||||
open: overflowOpen,
|
||||
hiddenCount: hiddenMembers().length
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div
|
||||
{...attrs}
|
||||
class="wire-next wire-next--avatar-group {class}"
|
||||
class="wrn-component wrn-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)'}"
|
||||
style="--wrn-avatar-group-columns:{columns}; --wrn-avatar-group-ring:{borderColor || 'var(--wrn-color-bg)'}"
|
||||
role="group"
|
||||
aria-label="Avatar group"
|
||||
>
|
||||
<div
|
||||
class="wire-next__avatar-group-members"
|
||||
class="wrn-next__avatar-group-members"
|
||||
>
|
||||
{#each visibleMembers() as item}
|
||||
<span
|
||||
class="wire-next__avatar-group-member"
|
||||
class="wrn-next__avatar-group-member"
|
||||
data-size="{item.size || size}"
|
||||
data-shape="{item.shape || shape}"
|
||||
data-color="{item.color || color}"
|
||||
@@ -70,7 +66,7 @@ component AvatarGroup {
|
||||
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
|
||||
>
|
||||
<span
|
||||
class="wire-next__avatar-group-avatar"
|
||||
class="wrn-next__avatar-group-avatar"
|
||||
>
|
||||
{#if item.src}
|
||||
<img
|
||||
@@ -95,7 +91,7 @@ component AvatarGroup {
|
||||
|
||||
{#if showTooltips && (item.tooltip || item.name)}
|
||||
<span
|
||||
class="wire-next__avatar-group-tooltip"
|
||||
class="wrn-next__avatar-group-tooltip"
|
||||
role="tooltip"
|
||||
>
|
||||
{item.tooltip || item.name}
|
||||
@@ -106,11 +102,11 @@ component AvatarGroup {
|
||||
|
||||
{#if hiddenMembers().length > 0}
|
||||
<span
|
||||
class="wire-next__avatar-group-overflow"
|
||||
class="wrn-next__avatar-group-overflow"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__avatar-group-overflow-button"
|
||||
class="wrn-next__avatar-group-overflow-button"
|
||||
aria-label="{overflowLabel}"
|
||||
aria-expanded="{overflowOpen ? 'true' : 'false'}"
|
||||
@click="toggleOverflow(event)"
|
||||
@@ -119,18 +115,18 @@ component AvatarGroup {
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="wire-next__avatar-group-menu"
|
||||
class="wrn-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"
|
||||
class="wrn-next__avatar-group-menu-item"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
class="wire-next__avatar-group-menu-avatar"
|
||||
class="wrn-next__avatar-group-menu-avatar"
|
||||
>
|
||||
{#if item.src}
|
||||
<img
|
||||
@@ -151,4 +147,279 @@ component AvatarGroup {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
/* --- Avatar group -------------------------------------------------------- */
|
||||
.wrn-next--avatar-group {
|
||||
--wrn-avatar-group-size: 3rem;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-size="xs"] {
|
||||
--wrn-avatar-group-size: 1.75rem;
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-size="sm"] {
|
||||
--wrn-avatar-group-size: 2.25rem;
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-size="md"],
|
||||
.wrn-next--avatar-group[data-size="default"] {
|
||||
--wrn-avatar-group-size: 3rem;
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-size="lg"] {
|
||||
--wrn-avatar-group-size: 4rem;
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-size="xl"] {
|
||||
--wrn-avatar-group-size: 5rem;
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-members {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-layout="stack"] .wrn-next__avatar-group-member,
|
||||
.wrn-next--avatar-group[data-layout="stack"] .wrn-next__avatar-group-overflow {
|
||||
margin-left: calc(var(--wrn-avatar-group-size) * -0.22);
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-layout="stack"] :first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.wrn-next--avatar-group[data-layout="grid"] .wrn-next__avatar-group-members {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--wrn-avatar-group-columns), var(--wrn-avatar-group-size));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member,
|
||||
.wrn-next__avatar-group-overflow {
|
||||
--wrn-avatar-group-color: var(--wrn-color-primary);
|
||||
position: relative;
|
||||
display: inline-grid;
|
||||
width: var(--wrn-avatar-group-size);
|
||||
height: var(--wrn-avatar-group-size);
|
||||
flex: none;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
outline: none;
|
||||
transition:
|
||||
z-index var(--wrn-motion-fast),
|
||||
transform var(--wrn-motion-base) var(--wrn-ease-emphasized);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member[data-color="secondary"] {
|
||||
--wrn-avatar-group-color: #737373;
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member[data-color="success"] {
|
||||
--wrn-avatar-group-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member[data-color="info"] {
|
||||
--wrn-avatar-group-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member[data-color="warning"] {
|
||||
--wrn-avatar-group-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member[data-color="danger"] {
|
||||
--wrn-avatar-group-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member[data-shape="rounded"] {
|
||||
border-radius: calc(var(--wrn-radius-sm, 0.5rem) * 0.72);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-avatar,
|
||||
.wrn-next__avatar-group-overflow-button {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
place-items: center;
|
||||
color: white;
|
||||
border: 2px solid var(--wrn-avatar-group-ring);
|
||||
border-radius: inherit;
|
||||
background: var(--wrn-avatar-group-color, var(--wrn-color-primary));
|
||||
box-shadow: 0 5px 14px rgb(0 0 0 / 0.16);
|
||||
font: inherit;
|
||||
font-size: calc(var(--wrn-avatar-group-size) * 0.3);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-avatar img,
|
||||
.wrn-next__avatar-group-menu-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-overflow-button {
|
||||
cursor: pointer;
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member:hover,
|
||||
.wrn-next__avatar-group-member:focus-visible,
|
||||
.wrn-next__avatar-group-overflow:focus-within {
|
||||
z-index: 5;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member:focus-visible,
|
||||
.wrn-next__avatar-group-overflow-button:focus-visible {
|
||||
outline: 2px solid var(--wrn-color-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-tooltip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 0.65rem);
|
||||
z-index: 20;
|
||||
width: max-content;
|
||||
max-width: 14rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
color: var(--wrn-color-bg);
|
||||
border-radius: 0.4rem;
|
||||
background: var(--wrn-color-text);
|
||||
box-shadow: 0 8px 22px rgb(0 0 0 / 0.22);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 650;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 0.25rem);
|
||||
transition:
|
||||
opacity var(--wrn-motion-fast),
|
||||
transform var(--wrn-motion-fast);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-member:hover .wrn-next__avatar-group-tooltip,
|
||||
.wrn-next__avatar-group-member:focus-visible .wrn-next__avatar-group-tooltip {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.65rem);
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
width: max-content;
|
||||
min-width: 12rem;
|
||||
gap: 0.25rem;
|
||||
padding: 0.45rem;
|
||||
color: var(--wrn-color-text);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm, 0.5rem);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: 0 16px 38px rgb(0 0 0 / 0.22);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-menu-item:hover {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
|
||||
.wrn-next__avatar-group-menu-avatar {
|
||||
display: grid;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: var(--wrn-color-surface-2);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ component Blockquote {
|
||||
view {
|
||||
<figure
|
||||
{...attrs}
|
||||
class="wire-next wire-next--blockquote wire-next--color-{color} wire-next--size-{size} {class}"
|
||||
class="wrn-component wrn-next--blockquote wrn-component--color-{color} wrn-component--size-{size} {class}"
|
||||
data-size="{size}"
|
||||
data-color="{color}"
|
||||
data-align="{align}"
|
||||
@@ -27,10 +27,10 @@ component Blockquote {
|
||||
>
|
||||
<blockquote cite="{citationUrl}">
|
||||
{#if quoteMark}
|
||||
<span class="wire-next__blockquote-mark" aria-hidden="true">“</span>
|
||||
<span class="wrn-next__blockquote-mark" aria-hidden="true">“</span>
|
||||
{/if}
|
||||
|
||||
<div class="wire-next__blockquote-copy">
|
||||
<div class="wrn-next__blockquote-copy">
|
||||
{#if quote}
|
||||
<p>{quote}</p>
|
||||
{:else}
|
||||
@@ -40,17 +40,17 @@ component Blockquote {
|
||||
</blockquote>
|
||||
|
||||
{#if citation || citationTitle || avatarSrc}
|
||||
<figcaption class="wire-next__blockquote-citation">
|
||||
<figcaption class="wrn-next__blockquote-citation">
|
||||
{#if avatarSrc}
|
||||
<img
|
||||
class="wire-next__blockquote-avatar"
|
||||
class="wrn-next__blockquote-avatar"
|
||||
src="{avatarSrc}"
|
||||
alt="{avatarAlt}"
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span class="wire-next__blockquote-attribution">
|
||||
<span class="wrn-next__blockquote-attribution">
|
||||
{#if citation}
|
||||
{#if citationUrl}
|
||||
<cite><a href="{citationUrl}">{citation}</a></cite>
|
||||
@@ -64,4 +64,276 @@ component Blockquote {
|
||||
{/if}
|
||||
</figure>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--blockquote {
|
||||
--wrn-blockquote-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
--wrn-blockquote-font-size: 1.125rem;
|
||||
--wrn-blockquote-mark-size: 4.25rem;
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
width: 100%;
|
||||
color: var(--wrn-color-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-size="xs"] {
|
||||
--wrn-blockquote-font-size: 0.875rem;
|
||||
--wrn-blockquote-mark-size: 3rem;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-size="sm"] {
|
||||
--wrn-blockquote-font-size: 1rem;
|
||||
--wrn-blockquote-mark-size: 3.5rem;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-size="md"],
|
||||
.wrn-next--blockquote[data-size="default"] {
|
||||
--wrn-blockquote-font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-size="lg"] {
|
||||
--wrn-blockquote-font-size: clamp(1.35rem, 2.4vw, 1.75rem);
|
||||
--wrn-blockquote-mark-size: 5rem;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-size="xl"] {
|
||||
--wrn-blockquote-font-size: clamp(1.65rem, 3vw, 2.25rem);
|
||||
--wrn-blockquote-mark-size: 5.75rem;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote > blockquote {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding-top: calc(var(--wrn-blockquote-mark-size) * 0.78);
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-mark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: color-mix(in srgb, var(--wrn-blockquote-color) 28%, var(--wrn-color-text));
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: var(--wrn-blockquote-mark-size);
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 0.8;
|
||||
text-align: start;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-copy {
|
||||
min-width: 0;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: var(--wrn-blockquote-font-size);
|
||||
line-height: 1.55;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-italic="true"] .wrn-next__blockquote-copy {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-copy > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-copy > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-citation {
|
||||
display: inline-flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
margin-inline-start: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
flex: 0 0 auto;
|
||||
border: 2px solid color-mix(in srgb, var(--wrn-blockquote-color) 35%, transparent);
|
||||
border-radius: 999px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-attribution {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-attribution cite {
|
||||
color: var(--wrn-color-text);
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-attribution a {
|
||||
color: inherit;
|
||||
text-decoration-color: color-mix(in srgb, var(--wrn-blockquote-color) 55%, transparent);
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
.wrn-next__blockquote-attribution a:hover {
|
||||
color: var(--wrn-blockquote-color);
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-align="center"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-align="right"],
|
||||
.wrn-next--blockquote[data-align="end"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-align="center"] > blockquote,
|
||||
.wrn-next--blockquote[data-align="right"] > blockquote,
|
||||
.wrn-next--blockquote[data-align="end"] > blockquote {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-align="center"] .wrn-next__blockquote-mark {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-align="right"] .wrn-next__blockquote-mark,
|
||||
.wrn-next--blockquote[data-align="end"] .wrn-next__blockquote-mark {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-align="center"] .wrn-next__blockquote-citation {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-align="right"] .wrn-next__blockquote-citation,
|
||||
.wrn-next--blockquote[data-align="end"] .wrn-next__blockquote-citation {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-variant="bordered"],
|
||||
.wrn-next--blockquote[data-variant="border"],
|
||||
.wrn-next--blockquote[data-variant="left-border"] {
|
||||
padding-inline-start: 1.5rem;
|
||||
border-inline-start: 4px solid var(--wrn-blockquote-color);
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-variant="bordered"] .wrn-next__blockquote-mark,
|
||||
.wrn-next--blockquote[data-variant="border"] .wrn-next__blockquote-mark,
|
||||
.wrn-next--blockquote[data-variant="left-border"] .wrn-next__blockquote-mark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-variant="bordered"] > blockquote,
|
||||
.wrn-next--blockquote[data-variant="border"] > blockquote,
|
||||
.wrn-next--blockquote[data-variant="left-border"] > blockquote {
|
||||
display: block;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-variant="bordered"] .wrn-next__blockquote-copy,
|
||||
.wrn-next--blockquote[data-variant="border"] .wrn-next__blockquote-copy,
|
||||
.wrn-next--blockquote[data-variant="left-border"] .wrn-next__blockquote-copy {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-variant="bordered"] .wrn-next__blockquote-citation,
|
||||
.wrn-next--blockquote[data-variant="border"] .wrn-next__blockquote-citation,
|
||||
.wrn-next--blockquote[data-variant="left-border"] .wrn-next__blockquote-citation {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.wrn-next--blockquote {
|
||||
--wrn-blockquote-mark-size: 3.25rem;
|
||||
--wrn-blockquote-font-size: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wrn-next--blockquote[data-size="lg"],
|
||||
.wrn-next--blockquote[data-size="xl"] {
|
||||
--wrn-blockquote-font-size: 1.25rem;
|
||||
}
|
||||
.wrn-next__blockquote-citation {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,34 +26,34 @@ label: string = "Breadcrumb"
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
aria-label='{label}'
|
||||
class='wire-breadcrumb {class}'
|
||||
class='wrn-breadcrumb {class}'
|
||||
>
|
||||
<ol class="wire-breadcrumb__list">
|
||||
<ol class="wrn-breadcrumb__list">
|
||||
{#if showHome}
|
||||
<li class="wire-breadcrumb__item wire-breadcrumb__item--home">
|
||||
<li class="wrn-breadcrumb__item wrn-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 } }))'
|
||||
class="wrn-breadcrumb__link wrn-breadcrumb__home"
|
||||
@click='output.select({ 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>
|
||||
<span class="icon-[lucide--house] wrn-breadcrumb__icon" aria-hidden="true"></span>
|
||||
{:else if homeIcon}
|
||||
<span class='{homeIcon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
<span class='{homeIcon} wrn-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__home-label">{homeLabel}</span>
|
||||
<span class="wrn-breadcrumb__home-label">{homeLabel}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
{#each items as item, itemIndex}
|
||||
<li
|
||||
class="wire-breadcrumb__item"
|
||||
class="wrn-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">
|
||||
<span class="wrn-breadcrumb__separator" aria-hidden="true">
|
||||
{#if separator === "slash"}
|
||||
<span>/</span>
|
||||
{:else if separator === "dot"}
|
||||
@@ -61,7 +61,7 @@ label: string = "Breadcrumb"
|
||||
{:else if separator === "arrow"}
|
||||
<span>→</span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--chevron-right] wire-breadcrumb__separator-icon"></span>
|
||||
<span class="icon-[lucide--chevron-right] wrn-breadcrumb__separator-icon"></span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -71,30 +71,30 @@ label: string = "Breadcrumb"
|
||||
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 } }))'
|
||||
class="wrn-breadcrumb__link"
|
||||
@click='output.select({ item: item, itemIndex: itemIndex })'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
<span class='{item.icon} wrn-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
|
||||
<span class="wrn-breadcrumb__label">{item.label || item.title}</span>
|
||||
|
||||
{#if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-breadcrumb__external" aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--arrow-up-right] wrn-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" : ""}'
|
||||
class="wrn-breadcrumb__current"
|
||||
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
<span class='{item.icon} wrn-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
|
||||
<span class="wrn-breadcrumb__label">{item.label || item.title}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
@@ -104,52 +104,52 @@ label: string = "Breadcrumb"
|
||||
}
|
||||
|
||||
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);
|
||||
.wrn-breadcrumb {
|
||||
--breadcrumb-accent: var(--wrn-color-primary);
|
||||
--breadcrumb-soft: var(--wrn-color-primary-soft);
|
||||
--breadcrumb-muted: var(--wrn-color-primary-muted);
|
||||
--breadcrumb-text: var(--wrn-color-primary-text);
|
||||
|
||||
min-width: 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
color: var(--wrn-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);
|
||||
.wrn-breadcrumb[data-color="secondary"] {
|
||||
--breadcrumb-accent: var(--wrn-color-secondary);
|
||||
--breadcrumb-soft: var(--wrn-color-secondary-soft);
|
||||
--breadcrumb-muted: var(--wrn-color-secondary-muted);
|
||||
--breadcrumb-text: var(--wrn-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);
|
||||
.wrn-breadcrumb[data-color="info"] {
|
||||
--breadcrumb-accent: var(--wrn-color-info);
|
||||
--breadcrumb-soft: var(--wrn-color-info-soft);
|
||||
--breadcrumb-muted: var(--wrn-color-info-muted);
|
||||
--breadcrumb-text: var(--wrn-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);
|
||||
.wrn-breadcrumb[data-color="success"] {
|
||||
--breadcrumb-accent: var(--wrn-color-success);
|
||||
--breadcrumb-soft: var(--wrn-color-success-soft);
|
||||
--breadcrumb-muted: var(--wrn-color-success-muted);
|
||||
--breadcrumb-text: var(--wrn-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);
|
||||
.wrn-breadcrumb[data-color="warning"] {
|
||||
--breadcrumb-accent: var(--wrn-color-warning);
|
||||
--breadcrumb-soft: var(--wrn-color-warning-soft);
|
||||
--breadcrumb-muted: var(--wrn-color-warning-muted);
|
||||
--breadcrumb-text: var(--wrn-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);
|
||||
.wrn-breadcrumb[data-color="danger"] {
|
||||
--breadcrumb-accent: var(--wrn-color-danger);
|
||||
--breadcrumb-soft: var(--wrn-color-danger-soft);
|
||||
--breadcrumb-muted: var(--wrn-color-danger-muted);
|
||||
--breadcrumb-text: var(--wrn-color-danger-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="soft"] {
|
||||
.wrn-breadcrumb[data-variant="soft"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
@@ -158,21 +158,21 @@ label: string = "Breadcrumb"
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="outline"] {
|
||||
.wrn-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);
|
||||
background: var(--wrn-color-surface-raised);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: var(--wire-shadow-sm);
|
||||
box-shadow: var(--wrn-shadow-sm);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] {
|
||||
.wrn-breadcrumb[data-variant="contrast"] {
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__list {
|
||||
.wrn-breadcrumb__list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
@@ -185,34 +185,34 @@ label: string = "Breadcrumb"
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__list::-webkit-scrollbar {
|
||||
.wrn-breadcrumb__list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__item {
|
||||
.wrn-breadcrumb__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__separator {
|
||||
.wrn-breadcrumb__separator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-inline: 0.45rem;
|
||||
color: var(--wire-color-text-subtle);
|
||||
color: var(--wrn-color-text-subtle);
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__separator-icon {
|
||||
.wrn-breadcrumb__separator-icon {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link,
|
||||
.wire-breadcrumb__current {
|
||||
.wrn-breadcrumb__link,
|
||||
.wrn-breadcrumb__current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
@@ -225,97 +225,97 @@ label: string = "Breadcrumb"
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__link,
|
||||
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__current {
|
||||
.wrn-breadcrumb[data-size="sm"] .wrn-breadcrumb__link,
|
||||
.wrn-breadcrumb[data-size="sm"] .wrn-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 {
|
||||
.wrn-breadcrumb[data-size="lg"] .wrn-breadcrumb__link,
|
||||
.wrn-breadcrumb[data-size="lg"] .wrn-breadcrumb__current {
|
||||
min-height: 2.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link {
|
||||
color: var(--wire-color-text-muted);
|
||||
.wrn-breadcrumb__link {
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-weight: 600;
|
||||
transition:
|
||||
color 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link:hover {
|
||||
.wrn-breadcrumb__link:hover {
|
||||
color: var(--breadcrumb-accent);
|
||||
background: var(--breadcrumb-soft);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link:focus-visible {
|
||||
.wrn-breadcrumb__link:focus-visible {
|
||||
color: var(--breadcrumb-accent);
|
||||
outline: 2px solid var(--breadcrumb-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current {
|
||||
.wrn-breadcrumb__current {
|
||||
max-width: 22rem;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-color-text);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
|
||||
.wrn-breadcrumb__item[data-current="true"] .wrn-breadcrumb__current {
|
||||
color: var(--breadcrumb-text);
|
||||
background: var(--breadcrumb-soft);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current[aria-disabled="true"] {
|
||||
.wrn-breadcrumb__current[aria-disabled="true"] {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__icon,
|
||||
.wire-breadcrumb__external {
|
||||
.wrn-breadcrumb__icon,
|
||||
.wrn-breadcrumb__external {
|
||||
flex: 0 0 auto;
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__home {
|
||||
.wrn-breadcrumb__home {
|
||||
color: var(--breadcrumb-accent);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__label {
|
||||
.wrn-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 {
|
||||
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__link,
|
||||
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__current,
|
||||
.wrn-breadcrumb[data-variant="contrast"] .wrn-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 {
|
||||
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__link:hover,
|
||||
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__item[data-current="true"] .wrn-breadcrumb__current {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__separator {
|
||||
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__separator {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-breadcrumb__home-label {
|
||||
.wrn-breadcrumb__home-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current {
|
||||
.wrn-breadcrumb__current {
|
||||
max-width: 13rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-breadcrumb__link {
|
||||
.wrn-breadcrumb__link {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ items: unknown[] = []
|
||||
view {
|
||||
<div
|
||||
{...attrs}
|
||||
class="wire-next wire-next--button-group {class}"
|
||||
class="wrn-component wrn-next--button-group {class}"
|
||||
role="{toolbar ? 'toolbar' : 'group'}"
|
||||
aria-label="{ariaLabel}"
|
||||
aria-orientation="{orientation}"
|
||||
@@ -71,7 +71,7 @@ items: unknown[] = []
|
||||
{#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}"
|
||||
class="wrn-btn wrn-btn--variant-{item.variant || variant} wrn-btn--color-{item.color || color} wrn-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') : ''}"
|
||||
@@ -80,13 +80,225 @@ items: unknown[] = []
|
||||
@click="selectItem(item, index)"
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class="wire-btn__icon {item.icon}" aria-hidden="true"></span>
|
||||
<span class="wrn-btn__icon {item.icon}" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-btn__label">{item.label}</span>
|
||||
<span class="wrn-btn__label">{item.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--button-group {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-orientation="vertical"] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-attached="false"] {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-attached="true"] > :is(.wrn-btn, .wrn-action) {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"] > .wrn-action {
|
||||
display: flex;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"] > :is(.wrn-btn, .wrn-action) :is(.wrn-btn) {
|
||||
height: 100%;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
|
||||
> :is(.wrn-btn, .wrn-action):not(:first-child) {
|
||||
margin-inline-start: -1px;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
|
||||
> .wrn-btn:not(:first-child):not(:last-child),
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
|
||||
> .wrn-action:not(:first-child):not(:last-child)
|
||||
> .wrn-btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
|
||||
> .wrn-btn:first-child:not(:last-child),
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
|
||||
> .wrn-action:first-child:not(:last-child)
|
||||
> .wrn-btn {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
|
||||
> .wrn-btn:last-child:not(:first-child),
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
|
||||
> .wrn-action:last-child:not(:first-child)
|
||||
> .wrn-btn {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
|
||||
> :is(.wrn-btn, .wrn-action):not(:first-child) {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
|
||||
> .wrn-btn:not(:first-child):not(:last-child),
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
|
||||
> .wrn-action:not(:first-child):not(:last-child)
|
||||
> .wrn-btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
|
||||
> .wrn-btn:first-child:not(:last-child),
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
|
||||
> .wrn-action:first-child:not(:last-child)
|
||||
> .wrn-btn {
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
|
||||
> .wrn-btn:last-child:not(:first-child),
|
||||
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
|
||||
> .wrn-action:last-child:not(:first-child)
|
||||
> .wrn-btn {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
.wrn-next--button-group > :is(.wrn-btn, .wrn-action):focus-within,
|
||||
.wrn-next--button-group > .wrn-btn:focus-visible {
|
||||
z-index: 2;
|
||||
}
|
||||
.wrn-next--button-group[data-selectable="true"] > .wrn-btn[data-selected="true"] {
|
||||
z-index: 1;
|
||||
border-color: var(--wrn-component-color);
|
||||
background: color-mix(in srgb, var(--wrn-component-color) 16%, var(--wrn-color-surface));
|
||||
color: var(--wrn-component-color);
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.wrn-next--button-group[data-responsive="true"] > :is(.wrn-btn, .wrn-action) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-responsive="true"] > .wrn-action > .wrn-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
|
||||
> :is(.wrn-btn, .wrn-action) {
|
||||
margin-top: -1px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
|
||||
> :is(.wrn-btn, .wrn-action):first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"] > .wrn-btn,
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
|
||||
> .wrn-action
|
||||
> .wrn-btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"] > .wrn-btn:first-child,
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
|
||||
> .wrn-action:first-child
|
||||
> .wrn-btn {
|
||||
border-start-start-radius: var(--wrn-radius-sm);
|
||||
border-start-end-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"] > .wrn-btn:last-child,
|
||||
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
|
||||
> .wrn-action:last-child
|
||||
> .wrn-btn {
|
||||
border-end-start-radius: var(--wrn-radius-sm);
|
||||
border-end-end-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ component Carousel {
|
||||
if (!sourceEvent || !sourceEvent.currentTarget) {
|
||||
return
|
||||
}
|
||||
root = sourceEvent.currentTarget.closest(".wire-next--carousel")
|
||||
root = sourceEvent.currentTarget.closest(".wrn-next--carousel")
|
||||
if (root) {
|
||||
carouselRoot = root
|
||||
}
|
||||
@@ -92,8 +92,8 @@ component Carousel {
|
||||
return
|
||||
}
|
||||
if (isSnap) {
|
||||
viewport = root.querySelector(".wire-next__carousel-viewport")
|
||||
slides = root.querySelectorAll(".wire-next__carousel-slide")
|
||||
viewport = root.querySelector(".wrn-next__carousel-viewport")
|
||||
slides = root.querySelectorAll(".wrn-next__carousel-slide")
|
||||
slide = slides[index]
|
||||
if (viewport && slide) {
|
||||
viewport.scrollTo({
|
||||
@@ -102,7 +102,7 @@ component Carousel {
|
||||
})
|
||||
}
|
||||
}
|
||||
thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button")
|
||||
thumbnails = root.querySelectorAll(".wrn-next__carousel-thumbnails button")
|
||||
if (thumbnails[index]) {
|
||||
thumbnail = thumbnails[index]
|
||||
rail = thumbnail.parentElement
|
||||
@@ -120,7 +120,7 @@ component Carousel {
|
||||
}
|
||||
rememberRoot(sourceEvent)
|
||||
viewport = sourceEvent.currentTarget
|
||||
slides = viewport.querySelectorAll(".wire-next__carousel-slide")
|
||||
slides = viewport.querySelectorAll(".wrn-next__carousel-slide")
|
||||
viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2
|
||||
closestIndex = currentIndex
|
||||
closestDistance = 1000000000
|
||||
@@ -142,7 +142,7 @@ component Carousel {
|
||||
item: items[currentIndex],
|
||||
reason: "snap"
|
||||
})
|
||||
slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button")
|
||||
slides = carouselRoot.querySelectorAll(".wrn-next__carousel-thumbnails button")
|
||||
slide = slides[currentIndex]
|
||||
if (slide) {
|
||||
viewport = slide.parentElement
|
||||
@@ -316,7 +316,7 @@ component Carousel {
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
class="wire-next wire-next--carousel wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}"
|
||||
class="wrn-component wrn-next--carousel wrn-component--color-{color} wrn-component--size-{size} wrn-next--variant-{variant} {class}"
|
||||
data-rtl="{isRTL}"
|
||||
data-centered="{isCentered}"
|
||||
data-draggable="{isDraggable && !isSnap}"
|
||||
@@ -335,15 +335,15 @@ component Carousel {
|
||||
@focusout="startAutoplay()"
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="wire-next__carousel-header">
|
||||
<header class="wrn-next__carousel-header">
|
||||
{#if title}<h3>{title}</h3>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<div class="wire-next__carousel-layout">
|
||||
<div class="wrn-next__carousel-layout">
|
||||
{#if thumbnails === "vertical"}
|
||||
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
|
||||
<div class="wrn-next__carousel-thumbnails" aria-label="Choose a slide">
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
@@ -359,10 +359,10 @@ component Carousel {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="wire-next__carousel-main">
|
||||
<div class="wire-next__carousel-stage">
|
||||
<div class="wrn-next__carousel-main">
|
||||
<div class="wrn-next__carousel-stage">
|
||||
<div
|
||||
class="wire-next__carousel-viewport"
|
||||
class="wrn-next__carousel-viewport"
|
||||
tabindex="0"
|
||||
@scroll="handleSnapScroll(event)"
|
||||
@pointerdown="beginDrag(event)"
|
||||
@@ -372,12 +372,12 @@ component Carousel {
|
||||
@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"
|
||||
class="wrn-next__carousel-track"
|
||||
style="--wrn-carousel-index: {currentIndex}; --wrn-carousel-per-view: {slidesPerView}; --wrn-carousel-gap: {gap}; --wrn-carousel-drag-offset: {dragOffset}px"
|
||||
>
|
||||
{#each items as item, index}
|
||||
<article
|
||||
class="wire-next__carousel-slide"
|
||||
class="wrn-next__carousel-slide"
|
||||
data-active="{index === currentIndex}"
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
@@ -387,7 +387,7 @@ component Carousel {
|
||||
{#if item.imageSrc}
|
||||
<img src="{item.imageSrc}" alt="{item.imageAlt || item.title || ''}" />
|
||||
{/if}
|
||||
<div class="wire-next__carousel-slide-content">
|
||||
<div class="wrn-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}
|
||||
@@ -402,7 +402,7 @@ component Carousel {
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="wire-next__carousel-control wire-next__carousel-control--previous"
|
||||
class="wrn-next__carousel-control wrn-next__carousel-control--previous"
|
||||
type="button"
|
||||
aria-label="Previous slide"
|
||||
disabled="{!isInfiniteLoop && currentIndex === 0}"
|
||||
@@ -411,7 +411,7 @@ component Carousel {
|
||||
<span class="icon-[lucide--chevron-left]" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
class="wire-next__carousel-control wire-next__carousel-control--next"
|
||||
class="wrn-next__carousel-control wrn-next__carousel-control--next"
|
||||
type="button"
|
||||
aria-label="Next slide"
|
||||
disabled="{!isInfiniteLoop && currentIndex === maximumIndex()}"
|
||||
@@ -421,14 +421,14 @@ component Carousel {
|
||||
</button>
|
||||
|
||||
{#if showCounter}
|
||||
<output class="wire-next__carousel-counter" aria-live="polite">
|
||||
<output class="wrn-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">
|
||||
<div class="wrn-next__carousel-pagination" aria-label="Choose a slide">
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
@@ -442,7 +442,7 @@ component Carousel {
|
||||
{/if}
|
||||
|
||||
{#if thumbnails === "horizontal"}
|
||||
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
|
||||
<div class="wrn-next__carousel-thumbnails" aria-label="Choose a slide">
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
@@ -461,4 +461,438 @@ component Carousel {
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--carousel {
|
||||
--wrn-carousel-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-header h3,
|
||||
.wrn-next__carousel-header p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-header p {
|
||||
margin-top: 0.35rem;
|
||||
color: var(--wrn-color-text-muted);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-layout {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-main {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-stage {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-viewport {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface-subtle, var(--wrn-color-surface));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-viewport:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-carousel-color) 30%, transparent);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-track {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: var(--wrn-carousel-gap);
|
||||
transform: translateX(
|
||||
calc(
|
||||
var(--wrn-carousel-index) * -1 *
|
||||
(
|
||||
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
|
||||
var(--wrn-carousel-per-view) + var(--wrn-carousel-gap)
|
||||
)
|
||||
)
|
||||
);
|
||||
transition: transform var(--wrn-motion-base) var(--wrn-ease-emphasized);
|
||||
translate: var(--wrn-carousel-drag-offset, 0) 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-dragging="true"] .wrn-next__carousel-track {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-rtl="true"] .wrn-next__carousel-track {
|
||||
transform: translateX(
|
||||
calc(
|
||||
var(--wrn-carousel-index) *
|
||||
(
|
||||
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
|
||||
var(--wrn-carousel-per-view) + var(--wrn-carousel-gap)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-slide {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: 0 0
|
||||
calc(
|
||||
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
|
||||
var(--wrn-carousel-per-view)
|
||||
);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 20rem;
|
||||
place-items: center;
|
||||
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-auto-height="true"] .wrn-next__carousel-slide {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-slide {
|
||||
flex-basis: min(78%, 32rem);
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-track {
|
||||
box-sizing: border-box;
|
||||
transform: translateX(
|
||||
calc(var(--wrn-carousel-index) * -1 * (min(78%, 32rem) + var(--wrn-carousel-gap)))
|
||||
);
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-slide:first-child {
|
||||
margin-inline-start: calc((100% - min(78%, 32rem)) / 2);
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-slide:last-child {
|
||||
margin-inline-end: calc((100% - min(78%, 32rem)) / 2);
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-centered="true"][data-rtl="true"] .wrn-next__carousel-track {
|
||||
transform: translateX(
|
||||
calc(var(--wrn-carousel-index) * (min(78%, 32rem) + var(--wrn-carousel-gap)))
|
||||
);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-slide > img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-slide-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(100%, 38rem);
|
||||
padding: clamp(2rem, 8vw, 5rem);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-slide-content h4,
|
||||
.wrn-next__carousel-slide-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-slide-content h4 {
|
||||
font-size: clamp(1.5rem, 4vw, 2.4rem);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-slide-content p {
|
||||
margin-top: 0.65rem;
|
||||
color: var(--wrn-color-text-muted);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-control {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 50%;
|
||||
display: grid;
|
||||
width: 2.75rem;
|
||||
min-height: 2.75rem;
|
||||
padding: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--wrn-color-border) 70%, transparent);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wrn-color-text) 76%, transparent);
|
||||
color: var(--wrn-color-surface);
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-control:focus-visible,
|
||||
.wrn-next__carousel-pagination button:focus-visible,
|
||||
.wrn-next__carousel-thumbnails button:focus-visible {
|
||||
outline: 2px solid var(--wrn-carousel-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-control:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-control--previous {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-control--next {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-rtl="true"] .wrn-next__carousel-control--previous {
|
||||
right: 1rem;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-rtl="true"] .wrn-next__carousel-control--next {
|
||||
right: auto;
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-pagination {
|
||||
display: flex;
|
||||
min-height: 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-pagination button {
|
||||
width: 0.65rem;
|
||||
min-height: 0.65rem;
|
||||
padding: 0;
|
||||
border: 1px solid var(--wrn-color-text-muted);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-pagination button[data-active="true"] {
|
||||
border-color: var(--wrn-carousel-color);
|
||||
background: var(--wrn-carousel-color);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-counter {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
bottom: 0.8rem;
|
||||
left: 50%;
|
||||
padding: 0.25rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wrn-color-text) 82%, transparent);
|
||||
color: var(--wrn-color-surface);
|
||||
font-weight: 700;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-thumbnails {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
gap: 0.55rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-thumbnails button {
|
||||
display: grid;
|
||||
flex: 0 0 8.5rem;
|
||||
min-height: 3.75rem;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-thumbnails button[data-active="true"] {
|
||||
border-color: var(--wrn-carousel-color);
|
||||
box-shadow: inset 0 0 0 1px var(--wrn-carousel-color);
|
||||
}
|
||||
|
||||
.wrn-next__carousel-thumbnails img {
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wrn-next__carousel-thumbnails span {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails {
|
||||
flex: 0 0 9rem;
|
||||
flex-direction: column;
|
||||
max-height: 28rem;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails button {
|
||||
flex-basis: 7.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-draggable="true"] .wrn-next__carousel-viewport {
|
||||
cursor: grab;
|
||||
touch-action: pan-y;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-draggable="true"] .wrn-next__carousel-viewport:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-draggable="true"] img,
|
||||
.wrn-next--carousel[data-draggable="true"] a {
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-snap="true"] .wrn-next__carousel-viewport {
|
||||
overflow-x: auto;
|
||||
scroll-behavior: smooth;
|
||||
scroll-snap-type: x mandatory;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-snap="true"] .wrn-next__carousel-track {
|
||||
--wrn-carousel-snap-slide: calc(
|
||||
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
|
||||
var(--wrn-carousel-per-view)
|
||||
);
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
box-sizing: content-box;
|
||||
grid-auto-columns: var(--wrn-carousel-snap-slide);
|
||||
grid-auto-flow: column;
|
||||
padding-inline: calc((100% - var(--wrn-carousel-snap-slide)) / 2);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wrn-next--carousel[data-snap="true"] .wrn-next__carousel-slide {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next__carousel-slide {
|
||||
flex-basis: 100%;
|
||||
min-height: 16rem;
|
||||
}
|
||||
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-layout {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails {
|
||||
flex: auto;
|
||||
flex-direction: row;
|
||||
max-height: none;
|
||||
}
|
||||
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails button {
|
||||
flex-basis: 8.5rem;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wrn-next__carousel-track {
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,107 @@
|
||||
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[])
|
||||
select(payload: { item: object; index: number; sourceEvent?: Event })
|
||||
dataPointClick(payload: { item: object; index: number; sourceEvent?: Event })
|
||||
legendToggle(payload: { item: object; index: number; hidden: boolean; sourceEvent?: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Chart"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
valueKey: string = "value"
|
||||
labelKey: string = "label"
|
||||
height: number = 240
|
||||
showLegend: boolean = true
|
||||
showValues: boolean = true
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "bar"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state hiddenIndexes = []
|
||||
|
||||
functions {
|
||||
shared function numericValue(item) { return Math.max(0, Number(item[valueKey]) || 0) }
|
||||
shared function maximumValue() {
|
||||
return Math.max(1, ...items.map(function (item) { return numericValue(item) }))
|
||||
}
|
||||
shared function isHidden(index) { return hiddenIndexes.includes(index) }
|
||||
shared function barWidth() { return items.length ? Math.max(4, 80 / items.length) : 80 }
|
||||
shared function barX(index) { return 10 + index * (80 / Math.max(1, items.length)) }
|
||||
shared function barHeight(item) { return numericValue(item) / maximumValue() * 72 }
|
||||
shared function barY(item) { return 84 - barHeight(item) }
|
||||
|
||||
client function selectPoint(item, index, sourceEvent) {
|
||||
output.dataPointClick({ item: item, index: index, sourceEvent: sourceEvent })
|
||||
output.select({ item: item, index: index, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function toggleLegend(item, index, sourceEvent, next) {
|
||||
next = hiddenIndexes.slice()
|
||||
if (next.includes(index)) {
|
||||
next = next.filter(function (value) { return value !== index })
|
||||
} else {
|
||||
next.push(index)
|
||||
}
|
||||
hiddenIndexes = next
|
||||
output.legendToggle({ item: item, index: index, hidden: next.includes(index), sourceEvent: sourceEvent })
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
<figure {...attrs} data-ui-component="Chart" data-variant='{variant}' class='wrn-chart {class}'>
|
||||
{#if title || description}
|
||||
<figcaption class="wrn-chart__header">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<span>{description}</span>{/if}
|
||||
</figcaption>
|
||||
{/if}
|
||||
<svg class="wrn-chart__plot" viewBox="0 0 100 100" role="img" aria-label='{title}' style='height:{height}px'>
|
||||
<line x1="8" y1="84" x2="96" y2="84" class="wrn-chart__axis" />
|
||||
{#each items as item, index}
|
||||
<g data-hidden='{isHidden(index) ? "true" : "false"}'>
|
||||
<rect
|
||||
x='{barX(index)}' y='{barY(item)}' width='{barWidth()}' height='{barHeight(item)}'
|
||||
rx="1.5" class="wrn-chart__bar" role="button" tabindex="0"
|
||||
aria-label='{item[labelKey] + ": " + numericValue(item)}'
|
||||
@click='selectPoint(item, index, event)'
|
||||
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectPoint(item, index, event) }'
|
||||
/>
|
||||
{#if showValues}<text x='{barX(index) + barWidth() / 2}' y='{barY(item) - 3}' class="wrn-chart__value">{numericValue(item)}</text>{/if}
|
||||
<text x='{barX(index) + barWidth() / 2}' y="94" class="wrn-chart__label">{item[labelKey]}</text>
|
||||
</g>
|
||||
{/each}
|
||||
</svg>
|
||||
{#if showLegend}
|
||||
<div class="wrn-chart__legend" aria-label="Chart legend">
|
||||
{#each items as item, index}
|
||||
<button type="button" aria-pressed='{isHidden(index) ? "false" : "true"}' @click='toggleLegend(item, index, event)'>
|
||||
<span aria-hidden="true"></span>{item[labelKey]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</section>
|
||||
</figure>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-chart { display: grid; gap: 1rem; min-width: 0; margin: 0; padding: 1rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); color: var(--wrn-color-text); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); }
|
||||
.wrn-chart__header { display: grid; gap: 0.2rem; }
|
||||
.wrn-chart__header > span { color: var(--wrn-color-muted); font-size: 0.8rem; }
|
||||
.wrn-chart__plot { display: block; width: 100%; min-height: 12rem; overflow: visible; }
|
||||
.wrn-chart__axis { stroke: var(--wrn-color-border); stroke-width: 0.5; }
|
||||
.wrn-chart__bar { fill: var(--wrn-color-primary); cursor: pointer; transition: opacity var(--wrn-motion-fast), transform var(--wrn-motion-fast); transform-box: fill-box; transform-origin: bottom; }
|
||||
.wrn-chart__bar:hover { opacity: 0.82; transform: scaleY(1.02); }
|
||||
.wrn-chart__bar:focus-visible { outline: 1.5px solid var(--wrn-color-focus); outline-offset: 1px; }
|
||||
.wrn-chart__plot g[data-hidden="true"] { opacity: 0.18; pointer-events: none; }
|
||||
.wrn-chart__value, .wrn-chart__label { fill: var(--wrn-color-text); font-size: 3.5px; text-anchor: middle; }
|
||||
.wrn-chart__label { fill: var(--wrn-color-muted); }
|
||||
.wrn-chart__legend { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.wrn-chart__legend button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.5rem; border: 1px solid var(--wrn-color-border); border-radius: 999px; color: var(--wrn-color-text); background: transparent; font: inherit; font-size: 0.75rem; cursor: pointer; }
|
||||
.wrn-chart__legend button > span { width: 0.65rem; height: 0.65rem; border-radius: 0.2rem; background: var(--wrn-color-primary); }
|
||||
.wrn-chart__legend button[aria-pressed="false"] { opacity: 0.5; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,31 +64,31 @@ component ChatBubble {
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
|
||||
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--chat-bubble wrn-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">
|
||||
<header class="wrn-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">
|
||||
<div class="wrn-next__chat-thread">
|
||||
{#each items as item, index}
|
||||
<article
|
||||
class="wire-next__chat-message"
|
||||
class="wrn-next__chat-message"
|
||||
data-direction="{messageDirection(item)}"
|
||||
aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
|
||||
>
|
||||
<div class="wire-next__chat-row">
|
||||
<div class="wrn-next__chat-row">
|
||||
{#if showAvatars && (item.avatarSrc || item.avatarFallback)}
|
||||
<button
|
||||
class="wire-next__chat-avatar"
|
||||
class="wrn-next__chat-avatar"
|
||||
type="button"
|
||||
aria-label="{item.avatarLabel || item.author || 'Message author'}"
|
||||
@click="selectAvatar(event, item, index)"
|
||||
@@ -102,7 +102,7 @@ component ChatBubble {
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-next__chat-content"
|
||||
class="wrn-next__chat-content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectMessage(item, index)"
|
||||
@@ -129,7 +129,7 @@ component ChatBubble {
|
||||
</div>
|
||||
|
||||
{#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
|
||||
<footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
|
||||
<footer class="wrn-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}
|
||||
@@ -147,4 +147,272 @@ component ChatBubble {
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--chat-bubble.wrn-component--size-sm {
|
||||
--wrn-chat-padding: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.wrn-next--chat-bubble.wrn-component--size-lg {
|
||||
--wrn-chat-padding: 1.25rem;
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
|
||||
.wrn-next--chat-bubble {
|
||||
--wrn-chat-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
--wrn-chat-padding: 1rem;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 1rem;
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wrn-next__chat-header h3,
|
||||
.wrn-next__chat-header p,
|
||||
.wrn-next__chat-content h4,
|
||||
.wrn-next__chat-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrn-next__chat-header p {
|
||||
margin-top: 0.35rem;
|
||||
color: var(--wrn-color-text-muted);
|
||||
}
|
||||
|
||||
.wrn-next__chat-thread {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.wrn-next__chat-message {
|
||||
display: grid;
|
||||
width: min(78%, 34rem);
|
||||
min-width: 0;
|
||||
justify-self: start;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.wrn-next__chat-message[data-direction="outgoing"] {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.wrn-next--chat-bubble[data-one-sided="true"] .wrn-next__chat-message {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.wrn-next__chat-row {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-row {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.wrn-next--chat-bubble[data-one-sided="true"]
|
||||
.wrn-next__chat-message[data-direction="outgoing"]
|
||||
.wrn-next__chat-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.wrn-next__chat-content {
|
||||
min-width: 0;
|
||||
padding: var(--wrn-chat-padding);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 1rem;
|
||||
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
|
||||
color: var(--wrn-color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next__chat-content:focus-visible {
|
||||
outline: 2px solid var(--wrn-chat-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-content {
|
||||
border-color: var(--wrn-chat-color);
|
||||
background: var(--wrn-chat-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
}
|
||||
|
||||
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-content :is(h4, p, li, span) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wrn-next__chat-content h4 {
|
||||
margin-bottom: 0.65rem;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.wrn-next__chat-content p + p,
|
||||
.wrn-next__chat-content p + nav {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.wrn-next__chat-content ul {
|
||||
display: grid;
|
||||
margin: 0.55rem 0 0;
|
||||
padding-inline-start: 1.25rem;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.wrn-next__chat-content nav {
|
||||
display: grid;
|
||||
margin-top: 0.75rem;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.wrn-next__chat-content a {
|
||||
color: var(--wrn-chat-color);
|
||||
font-weight: 650;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-content a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.wrn-next__chat-avatar {
|
||||
display: grid;
|
||||
flex: 0 0 2.5rem;
|
||||
width: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
|
||||
color: var(--wrn-color-text);
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.wrn-next__chat-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wrn-next__chat-avatar:focus-visible {
|
||||
outline: 2px solid var(--wrn-chat-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-next__chat-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding-inline: calc(2.5rem + 0.75rem);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.78em;
|
||||
}
|
||||
|
||||
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-meta {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wrn-next__chat-meta[data-tone="danger"] {
|
||||
color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-next__chat-meta button {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next__chat-message {
|
||||
width: min(92%, 34rem);
|
||||
}
|
||||
.wrn-next__chat-avatar {
|
||||
flex-basis: 2rem;
|
||||
width: 2rem;
|
||||
min-height: 2rem;
|
||||
}
|
||||
.wrn-next__chat-meta {
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,97 @@
|
||||
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)
|
||||
copy(payload: { value: string; sourceEvent?: Event })
|
||||
success(payload: { value: string; sourceEvent?: Event })
|
||||
error(payload: { error: Error | string; value: string; sourceEvent?: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
value: string = ""
|
||||
label: string = "Copy"
|
||||
copiedLabel: string = "Copied"
|
||||
errorLabel: string = "Copy failed"
|
||||
title: string = "Clipboard"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
code: boolean = true
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
disabled: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state status = "idle"
|
||||
|
||||
functions {
|
||||
client function resetStatus() { status = "idle" }
|
||||
client function copySucceeded() {
|
||||
status = "success"
|
||||
output.success({ value: value })
|
||||
setTimeout(resetStatus, 1600)
|
||||
}
|
||||
client function copyFailed(error) {
|
||||
status = "error"
|
||||
output.error({ error: error, value: value })
|
||||
setTimeout(resetStatus, 1600)
|
||||
}
|
||||
client function copyValue(sourceEvent) {
|
||||
if (disabled || status === "copying") {
|
||||
return
|
||||
}
|
||||
status = "copying"
|
||||
output.copy({ value: value, sourceEvent: sourceEvent })
|
||||
if (!window.navigator.clipboard || !window.navigator.clipboard.writeText) {
|
||||
copyFailed(new Error("Clipboard API is unavailable"))
|
||||
return
|
||||
}
|
||||
return window.navigator.clipboard.writeText(value).then(copySucceeded).catch(copyFailed)
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="Clipboard"
|
||||
data-status='{status}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
class='wrn-clipboard {class}'
|
||||
>
|
||||
{#if title}<strong class="wrn-clipboard__title">{title}</strong>{/if}
|
||||
{#if description}<p class="wrn-clipboard__description">{description}</p>{/if}
|
||||
<div class="wrn-clipboard__control">
|
||||
{#if code}
|
||||
<code class="wrn-clipboard__value">{value}</code>
|
||||
{:else}
|
||||
<span class="wrn-clipboard__value">{value}</span>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="wrn-clipboard__button"
|
||||
disabled='{disabled}'
|
||||
aria-label='{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}'
|
||||
@click='copyValue(event)'
|
||||
>
|
||||
<span class="icon-[lucide--copy] wrn-clipboard__icon" data-show='status !== "success"' aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--check] wrn-clipboard__icon" data-show='status === "success"' aria-hidden="true"></span>
|
||||
<span>{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}</span>
|
||||
</button>
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-clipboard { display: grid; gap: 0.5rem; color: var(--wrn-color-text); }
|
||||
.wrn-clipboard__title { font-size: 0.875rem; }
|
||||
.wrn-clipboard__description { margin: 0; color: var(--wrn-color-muted); font-size: 0.8rem; }
|
||||
.wrn-clipboard__control { display: flex; min-width: 0; align-items: stretch; overflow: hidden; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); }
|
||||
.wrn-clipboard__value { min-width: 0; flex: 1; overflow: auto; padding: 0.7rem 0.85rem; color: var(--wrn-color-text); background: var(--wrn-color-surface-2); font: inherit; font-family: var(--wrn-font-mono, ui-monospace, monospace); white-space: nowrap; }
|
||||
.wrn-clipboard__button { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.6rem 0.85rem; border: 0; border-left: 1px solid var(--wrn-color-border); color: var(--wrn-color-primary); background: transparent; font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer; }
|
||||
.wrn-clipboard__button:hover { background: var(--wrn-color-primary-soft); }
|
||||
.wrn-clipboard__button:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: -3px; }
|
||||
.wrn-clipboard__button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.wrn-clipboard[data-status="success"] .wrn-clipboard__button { color: var(--wrn-color-success); }
|
||||
.wrn-clipboard[data-status="error"] .wrn-clipboard__button { color: var(--wrn-color-danger); }
|
||||
.wrn-clipboard__icon { width: 1rem; height: 1rem; flex: none; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,18 +51,18 @@ size: string = "default"
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--collapse {class}"
|
||||
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--collapse {class}"
|
||||
data-mode="{mode}"
|
||||
aria-label="{ariaLabel}"
|
||||
>
|
||||
{#each items as item, index}
|
||||
<article class="wire-next__collapse-item" data-open="{isOpen(index)}">
|
||||
<article class="wrn-next__collapse-item" data-open="{isOpen(index)}">
|
||||
{#if mode === "inline" && item.preview}
|
||||
<p class="wire-next__collapse-preview">{item.preview}</p>
|
||||
<p class="wrn-next__collapse-preview">{item.preview}</p>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="wire-next__collapse-trigger"
|
||||
class="wrn-next__collapse-trigger"
|
||||
type="button"
|
||||
aria-expanded="{isOpen(index) ? 'true' : 'false'}"
|
||||
aria-controls="{item.id || 'collapse-panel-' + index}"
|
||||
@@ -70,17 +70,17 @@ size: string = "default"
|
||||
@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>
|
||||
<span class="icon-[lucide--chevron-down] wrn-next__collapse-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="{item.id || 'collapse-panel-' + index}"
|
||||
class="wire-next__collapse-panel"
|
||||
class="wrn-next__collapse-panel"
|
||||
data-open="{isOpen(index)}"
|
||||
aria-hidden="{isOpen(index) ? 'false' : 'true'}"
|
||||
inert="{!isOpen(index)}"
|
||||
>
|
||||
<div class="wire-next__collapse-content">
|
||||
<div class="wrn-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}
|
||||
@@ -95,4 +95,221 @@ size: string = "default"
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--collapse.wrn-component--size-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.wrn-next--collapse.wrn-component--size-lg {
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
|
||||
.wrn-next--collapse {
|
||||
--wrn-collapse-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 0.75rem;
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wrn-next__collapse-item {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
justify-items: start;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.wrn-next__collapse-trigger {
|
||||
display: inline-flex;
|
||||
min-height: 2.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.7rem 1rem;
|
||||
border: 1px solid var(--wrn-collapse-color);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-collapse-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next__collapse-trigger:focus-visible {
|
||||
outline: 2px solid var(--wrn-collapse-color);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wrn-next__collapse-trigger:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.wrn-next__collapse-chevron {
|
||||
transition: transform var(--wrn-motion-fast) var(--wrn-ease-standard);
|
||||
}
|
||||
|
||||
.wrn-next__collapse-item[data-open="true"] .wrn-next__collapse-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.wrn-next__collapse-panel {
|
||||
width: min(100%, 42rem);
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
opacity: 0;
|
||||
transition:
|
||||
max-height 350ms var(--wrn-ease-emphasized),
|
||||
margin-top 300ms var(--wrn-ease-emphasized),
|
||||
opacity 200ms var(--wrn-ease-standard);
|
||||
}
|
||||
|
||||
.wrn-next__collapse-panel[data-open="true"] {
|
||||
max-height: 40rem;
|
||||
margin-top: 0.75rem;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wrn-next__collapse-content {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
gap: 0.65rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
|
||||
}
|
||||
|
||||
.wrn-next__collapse-content h4,
|
||||
.wrn-next__collapse-content p,
|
||||
.wrn-next__collapse-preview {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrn-next__collapse-content nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wrn-next__collapse-content a {
|
||||
color: var(--wrn-collapse-color);
|
||||
font-weight: 650;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-item {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-preview {
|
||||
order: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-panel {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-panel[data-open="true"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-trigger {
|
||||
order: 3;
|
||||
margin-top: 0.5rem;
|
||||
min-height: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--wrn-collapse-color);
|
||||
}
|
||||
|
||||
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-content {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wrn-next__collapse-chevron,
|
||||
.wrn-next__collapse-panel {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,576 @@ component ColorPicker {
|
||||
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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-component--color-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
|
||||
<div class="wrn-next__field-heading"><label class="{hiddenLabel ? 'wrn-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wrn-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="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next__color-control input[type="color"] {
|
||||
width: 3rem;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Shared field foundation. */
|
||||
|
||||
/* Validation states set by the browser validation runtime. */
|
||||
.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger) !important;
|
||||
}
|
||||
|
||||
.wrn-field-error {
|
||||
display: block;
|
||||
min-height: 1em;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.wrn-next--choice {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next--choice input {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
accent-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-next--field {
|
||||
display: grid;
|
||||
width: min(100%, 28rem);
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select) {
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.6rem;
|
||||
outline: 0;
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-bg);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-auth-form form > .wrn-next--field,
|
||||
.wrn-auth-form form > .wrn-field {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.wrn-auth-form__submit > .wrn-btn {
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select):focus-visible {
|
||||
border-color: var(--wrn-color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
|
||||
}
|
||||
.wrn-next--field textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
.wrn-next--field,
|
||||
.wrn-next--choice-field {
|
||||
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
width: min(100%, 34rem);
|
||||
min-width: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-sm,
|
||||
.wrn-next--choice-field.wrn-next--size-sm {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-lg,
|
||||
.wrn-next--choice-field.wrn-next--size-lg {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading label {
|
||||
color: var(--wrn-color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-hint,
|
||||
.wrn-next__field-help {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.82em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__field-control {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-inline: 0.85rem;
|
||||
border-color: var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
|
||||
padding-inline-start: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
|
||||
padding-inline-end: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
|
||||
right: 0.85rem;
|
||||
left: auto;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
padding-inline: 0;
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
|
||||
border-radius: 999px;
|
||||
padding-inline: 1.15rem;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__field-floating-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
transition: 150ms ease;
|
||||
}
|
||||
.wrn-next--textarea .wrn-next__field-floating-label {
|
||||
top: 1.35rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-top: 1.2rem;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
|
||||
+ .wrn-next__field-floating-label {
|
||||
top: 0.42rem;
|
||||
font-size: 0.7em;
|
||||
transform: none;
|
||||
}
|
||||
.wrn-next__field-error {
|
||||
min-height: 1em;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.82em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-error:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
|
||||
.wrn-next--choice-field[data-invalid="true"] input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):disabled,
|
||||
.wrn-next--choice-field input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):read-only {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
|
||||
display: flex;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
.wrn-next__sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next__choice-field {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.wrn-next__choice {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
color: var(--wrn-color-text);
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__checkbox-content > .wrn-next__choice {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.wrn-next__choice input {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
accent-color: var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next__choice-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.15rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrn-next__choice-copy small {
|
||||
color: var(--wrn-color-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__radio-group,
|
||||
.wrn-next__checkbox-group {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
|
||||
align-items: stretch;
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
|
||||
min-height: 4.5rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
|
||||
border-color: var(--wrn-field-color);
|
||||
box-shadow: 0 0 0 1px var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
|
||||
padding-block: 0.75rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
|
||||
order: 2;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
|
||||
order: 1;
|
||||
}
|
||||
.wrn-next__choice[data-variant="outlined"],
|
||||
.wrn-next__choice[data-variant="floating"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
.wrn-next__choice[data-variant="pill"] {
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__choice[data-variant="ghost"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
|
||||
}
|
||||
.wrn-next__choice[data-variant="underline"] {
|
||||
padding-block: 0.5rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--select .wrn-next__field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__color-control,
|
||||
.wrn-next__range-control,
|
||||
.wrn-next__input-group-control,
|
||||
.wrn-next__file-control {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next__picker-control,
|
||||
.wrn-next--time-picker .wrn-next__field-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__picker-control > button:first-of-type,
|
||||
.wrn-next__time-trigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.wrn-next__calendar,
|
||||
.wrn-next__time-panel {
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
width: min(100%, 22rem);
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-3);
|
||||
}
|
||||
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
|
||||
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next__calendar button,
|
||||
.wrn-next__time-panel button {
|
||||
min-height: 2.25rem;
|
||||
border: 0;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.wrn-next__calendar-weekdays,
|
||||
.wrn-next__calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.wrn-next__calendar-weekdays {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.wrn-next__calendar-grid button:hover,
|
||||
.wrn-next__time-panel button:hover {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next__calendar-grid button[aria-pressed="true"],
|
||||
.wrn-next__time-panel button[aria-pressed="true"] {
|
||||
background: var(--wrn-field-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
}
|
||||
.wrn-next__time-period {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row,
|
||||
.wrn-next__upload-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
|
||||
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
|
||||
}
|
||||
.wrn-next--strong-password.wrn-next--invalid input,
|
||||
.wrn-next--strong-password input.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// Columns -- a simple multi-column split for page content.
|
||||
//
|
||||
// <Columns columns={2} gap="lg">...</Columns>
|
||||
//
|
||||
// Columns and Grid overlap deliberately: Columns is the coarse two or three
|
||||
// way split of a page, and its gaps are wider for that reason. Grid is the one
|
||||
// to reach for when the items are cards and the count matters.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
component Columns {
|
||||
props {
|
||||
size: string = "default"
|
||||
@@ -8,28 +18,92 @@ component Columns {
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
shared function columnCount() {
|
||||
var value = Number(columns)
|
||||
if (!value || value < 1) {
|
||||
return 1
|
||||
}
|
||||
return Math.min(4, value)
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Columns"
|
||||
class='wrn-columns {class}'
|
||||
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"'
|
||||
data-max-width='{maxWidth}'
|
||||
data-columns='{columnCount()}'
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-columns {
|
||||
--columns-gap: 1.5rem;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: var(--columns-gap);
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.wrn-columns[data-max-width="md"] {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.wrn-columns[data-max-width="lg"] {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.wrn-columns[data-max-width="xl"] {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.wrn-columns[data-max-width="2xl"] {
|
||||
max-width: 96rem;
|
||||
}
|
||||
|
||||
.wrn-columns[data-max-width="full"] {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wrn-columns[data-gap="sm"] {
|
||||
--columns-gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wrn-columns[data-gap="lg"] {
|
||||
--columns-gap: 2.5rem;
|
||||
}
|
||||
|
||||
.wrn-columns[data-gap="xl"] {
|
||||
--columns-gap: 3.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Splits at the tablet breakpoint rather than the phone one: a page split
|
||||
* is worth keeping single-column for longer than a card deck is.
|
||||
*/
|
||||
@media (min-width: 768px) {
|
||||
.wrn-columns:not([data-columns="1"]) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wrn-columns[data-columns="3"] {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wrn-columns[data-columns="4"] {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import SelectStyles from "../styles/SelectStyles.wrn"
|
||||
|
||||
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)
|
||||
@@ -225,7 +227,7 @@ component ComboBox {
|
||||
|
||||
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}"
|
||||
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--advanced-select wrn-next--combobox {open ? 'wrn-next--open' : ''} {fixed ? 'wrn-next--advanced-select-fixed' : ''} {invalid ? 'wrn-next--invalid' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
|
||||
data-placement="{placement}"
|
||||
data-search-mode="{searchMode}"
|
||||
data-remote="{remote ? 'true' : 'false'}"
|
||||
@@ -241,12 +243,12 @@ component ComboBox {
|
||||
>
|
||||
<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>
|
||||
<div class="wrn-next__select-control wrn-next__combobox-control">
|
||||
<span class="wrn-next__search-icon icon-[lucide--search]" aria-hidden="true"></span>
|
||||
<input
|
||||
{...attrs}
|
||||
id="{name}-input"
|
||||
class="wire-next__select-trigger wire-next__combobox-input"
|
||||
class="wrn-next__select-trigger wrn-next__combobox-input"
|
||||
type="text"
|
||||
role="combobox"
|
||||
name="{allowCustomValue ? name : ''}"
|
||||
@@ -269,7 +271,7 @@ component ComboBox {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__clear-select"
|
||||
class="wrn-next__clear-select"
|
||||
data-show="clearable && inputText() && !disabled"
|
||||
aria-label="{clearLabel}"
|
||||
title="{clearLabel}"
|
||||
@@ -279,44 +281,44 @@ component ComboBox {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__combobox-toggle"
|
||||
class="wrn-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>
|
||||
<span class="wrn-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' : ''}"
|
||||
class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
|
||||
data-show="shouldShowDropdown()"
|
||||
style="{shouldShowDropdown() ? '' : 'display: none'}"
|
||||
>
|
||||
<div
|
||||
class="wire-next__select-message"
|
||||
class="wrn-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>
|
||||
<div class="wrn-next__select-message" data-show="loading" role="status">
|
||||
<i class="wrn-spinner wrn-spinner--inline" aria-hidden="true"></i>
|
||||
{loadingLabel}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="{name}-listbox"
|
||||
class="wire-next__select-list"
|
||||
class="wrn-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>
|
||||
<div class="wrn-next__option-group" role="group" aria-label="{group.label}">
|
||||
<div class="wrn-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' : ''}"
|
||||
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
|
||||
data-option-value="{option.value}"
|
||||
data-show="matches(option)"
|
||||
role="option"
|
||||
@@ -328,9 +330,9 @@ component ComboBox {
|
||||
>
|
||||
{#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}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wrn-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>
|
||||
<i class="wrn-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -340,7 +342,7 @@ component ComboBox {
|
||||
{#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' : ''}"
|
||||
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
|
||||
data-option-value="{option.value}"
|
||||
data-show="matches(option)"
|
||||
role="option"
|
||||
@@ -352,16 +354,16 @@ component ComboBox {
|
||||
>
|
||||
{#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}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wrn-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>
|
||||
<i class="wrn-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__load-more"
|
||||
class="wrn-next__load-more"
|
||||
data-wrn-select-load-more
|
||||
data-show="infinite && hasMore"
|
||||
>{loadMoreLabel}</button>
|
||||
@@ -381,9 +383,104 @@ component ComboBox {
|
||||
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
|
||||
<small
|
||||
id="{name}-validation"
|
||||
class="wire-next__validation"
|
||||
class="wrn-next__validation"
|
||||
data-error="{name}"
|
||||
>{validationMessage}</small>
|
||||
</div>
|
||||
<SelectStyles hidden />
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next__combobox-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
width: 2.25em;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wrn-next--combobox .wrn-next__combobox-input {
|
||||
padding-right: 5.25em;
|
||||
padding-left: 2.5em;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,72 @@
|
||||
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 })
|
||||
start(payload: { sourceEvent?: Event; duration: number })
|
||||
complete(payload: { sourceEvent?: Event; duration: number })
|
||||
}
|
||||
|
||||
props {
|
||||
label: string = "Celebrate"
|
||||
duration: number = 1200
|
||||
count: number = 24
|
||||
autoStart: boolean = false
|
||||
disabled: boolean = false
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Confetti"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state running = false
|
||||
|
||||
functions {
|
||||
shared function pieces() { return Array.from({ length: Math.max(1, Math.min(60, count)) }) }
|
||||
shared function pieceStyle(index) {
|
||||
return "--wrn-confetti-index:" + index + ";--wrn-confetti-x:" + ((index * 37) % 100) + ";--wrn-confetti-delay:" + ((index * 29) % 240) + "ms;"
|
||||
}
|
||||
client function finish(sourceEvent) {
|
||||
running = false
|
||||
output.complete({ sourceEvent: sourceEvent, duration: duration })
|
||||
}
|
||||
client function start(sourceEvent) {
|
||||
if (disabled || running) {
|
||||
return
|
||||
}
|
||||
running = true
|
||||
output.start({ sourceEvent: sourceEvent, duration: duration })
|
||||
setTimeout(finish, duration, sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
mount { if (autoStart) { start(null) } }
|
||||
}
|
||||
|
||||
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}
|
||||
<div {...attrs} data-ui-component="Confetti" data-running='{running ? "true" : "false"}' class='wrn-confetti {class}' style='--wrn-confetti-duration:{duration}ms'>
|
||||
<button type="button" class="wrn-confetti__trigger" disabled='{disabled}' @click='start(event)'>
|
||||
<span class="icon-[lucide--party-popper]" aria-hidden="true"></span><span>{label}</span>
|
||||
</button>
|
||||
<div class="wrn-confetti__burst" aria-hidden="true">
|
||||
{#each pieces() as piece, index}<i style='{pieceStyle(index)}'></i>{/each}
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-confetti { position: relative; display: inline-flex; }
|
||||
.wrn-confetti__trigger { display: inline-flex; min-height: 2.5rem; align-items: center; gap: 0.5rem; padding: 0.55rem 0.85rem; border: 1px solid var(--wrn-color-primary); border-radius: var(--wrn-radius); color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); font: inherit; font-weight: 700; cursor: pointer; }
|
||||
.wrn-confetti__trigger > span:first-child { width: 1rem; height: 1rem; }
|
||||
.wrn-confetti__trigger:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: 2px; }
|
||||
.wrn-confetti__trigger:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.wrn-confetti__burst { position: absolute; inset: 50% 0 auto; height: 0; pointer-events: none; }
|
||||
.wrn-confetti__burst > i { position: absolute; left: calc(var(--wrn-confetti-x) * 1%); width: 0.45rem; height: 0.7rem; border-radius: 0.1rem; opacity: 0; background: hsl(calc(var(--wrn-confetti-index) * 47deg) 80% 55%); transform: translate(-50%, 0) rotate(calc(var(--wrn-confetti-index) * 23deg)); }
|
||||
.wrn-confetti[data-running="true"] .wrn-confetti__burst > i { animation: wrn-confetti-burst var(--wrn-confetti-duration) cubic-bezier(0.2, 0.7, 0.2, 1) var(--wrn-confetti-delay) both; }
|
||||
@keyframes wrn-confetti-burst {
|
||||
0% { opacity: 1; transform: translate(-50%, 0) rotate(0); }
|
||||
100% { opacity: 0; transform: translate(calc(-50% + (var(--wrn-confetti-x) - 50) * 0.9px), 9rem) rotate(540deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wrn-confetti[data-running="true"] .wrn-confetti__burst > i { animation-duration: 1ms; animation-delay: 0ms; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ component ContextMenu {
|
||||
}
|
||||
|
||||
client function moveFocus(sourceEvent, direction) {
|
||||
const root = sourceEvent.currentTarget.closest(".wire-context-menu") || sourceEvent.currentTarget
|
||||
const root = sourceEvent.currentTarget.closest(".wrn-context-menu") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
|
||||
if (!options.length) {
|
||||
return
|
||||
@@ -137,12 +137,12 @@ component ContextMenu {
|
||||
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};'
|
||||
class='wrn-context-menu {class}'
|
||||
style='--wrn-context-x:{positionX}px;--wrn-context-y:{positionY}px;--wrn-context-min-width:{minWidth};--wrn-context-max-width:{maxWidth};'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<div
|
||||
class="wire-context-menu__trigger"
|
||||
class="wrn-context-menu__trigger"
|
||||
tabindex='{disabled ? "-1" : "0"}'
|
||||
aria-haspopup="menu"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@@ -156,7 +156,7 @@ component ContextMenu {
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-context-menu__dismiss-layer"
|
||||
class="wrn-context-menu__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label="Close context menu"
|
||||
@click='hideMenu("outside", event)'
|
||||
@@ -164,14 +164,14 @@ component ContextMenu {
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-context-menu__panel"
|
||||
class="wrn-context-menu__panel"
|
||||
data-wrn-anchored="true"
|
||||
data-show='{open || visible}'
|
||||
role="menu"
|
||||
aria-label='{label}'
|
||||
>
|
||||
{#if title || description}
|
||||
<div class="wire-context-menu__header">
|
||||
<div class="wrn-context-menu__header">
|
||||
{#if title}
|
||||
<strong>{title}</strong>
|
||||
{/if}
|
||||
@@ -183,12 +183,12 @@ component ContextMenu {
|
||||
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div class="wire-context-menu__items">
|
||||
<div class="wrn-context-menu__items">
|
||||
{#each items as item, itemIndex}
|
||||
{#if item.type === "divider"}
|
||||
<div class="wire-context-menu__divider" role="separator"></div>
|
||||
<div class="wrn-context-menu__divider" role="separator"></div>
|
||||
{:else if item.type === "header"}
|
||||
<div class="wire-context-menu__section-label">{item.label || item.title}</div>
|
||||
<div class="wrn-context-menu__section-label">{item.label || item.title}</div>
|
||||
{:else if item.href}
|
||||
<a
|
||||
href='{item.href}'
|
||||
@@ -199,13 +199,13 @@ component ContextMenu {
|
||||
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"
|
||||
class="wrn-context-menu__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
<span class='wrn-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-context-menu__copy">
|
||||
<span class="wrn-context-menu__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
@@ -214,9 +214,9 @@ component ContextMenu {
|
||||
{#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>
|
||||
<span class="icon-[lucide--check] wrn-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>
|
||||
<span class="icon-[lucide--arrow-up-right] wrn-context-menu__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
@@ -227,13 +227,13 @@ component ContextMenu {
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
disabled='{item.disabled}'
|
||||
class="wire-context-menu__item"
|
||||
class="wrn-context-menu__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
<span class='wrn-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-context-menu__copy">
|
||||
<span class="wrn-context-menu__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
@@ -242,7 +242,7 @@ component ContextMenu {
|
||||
{#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>
|
||||
<span class="icon-[lucide--check] wrn-context-menu__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -256,52 +256,52 @@ component ContextMenu {
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-context-menu {
|
||||
--context-accent: var(--wire-color-primary);
|
||||
--context-soft: var(--wire-color-primary-soft);
|
||||
.wrn-context-menu {
|
||||
--context-accent: var(--wrn-color-primary);
|
||||
--context-soft: var(--wrn-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);
|
||||
.wrn-context-menu[data-color="secondary"] {
|
||||
--context-accent: var(--wrn-color-secondary);
|
||||
--context-soft: var(--wrn-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="info"] {
|
||||
--context-accent: var(--wire-color-info);
|
||||
--context-soft: var(--wire-color-info-soft);
|
||||
.wrn-context-menu[data-color="info"] {
|
||||
--context-accent: var(--wrn-color-info);
|
||||
--context-soft: var(--wrn-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="success"] {
|
||||
--context-accent: var(--wire-color-success);
|
||||
--context-soft: var(--wire-color-success-soft);
|
||||
.wrn-context-menu[data-color="success"] {
|
||||
--context-accent: var(--wrn-color-success);
|
||||
--context-soft: var(--wrn-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="warning"] {
|
||||
--context-accent: var(--wire-color-warning);
|
||||
--context-soft: var(--wire-color-warning-soft);
|
||||
.wrn-context-menu[data-color="warning"] {
|
||||
--context-accent: var(--wrn-color-warning);
|
||||
--context-soft: var(--wrn-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="danger"] {
|
||||
--context-accent: var(--wire-color-danger);
|
||||
--context-soft: var(--wire-color-danger-soft);
|
||||
.wrn-context-menu[data-color="danger"] {
|
||||
--context-accent: var(--wrn-color-danger);
|
||||
--context-soft: var(--wrn-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu__trigger {
|
||||
.wrn-context-menu__trigger {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__trigger:focus-visible {
|
||||
.wrn-context-menu__trigger:focus-visible {
|
||||
border-radius: 0.6rem;
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline: 2px solid var(--wrn-color-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wire-context-menu__dismiss-layer {
|
||||
.wrn-context-menu__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1090;
|
||||
@@ -311,20 +311,20 @@ component ContextMenu {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu__panel {
|
||||
.wrn-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));
|
||||
min-width: var(--wrn-context-min-width);
|
||||
max-width: min(var(--wrn-context-max-width), calc(100vw - 1.5rem));
|
||||
padding: 0.45rem;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-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));
|
||||
color-mix(in srgb, var(--wrn-color-surface-raised) 96%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--context-accent) 18%, var(--wrn-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
@@ -333,25 +333,25 @@ component ContextMenu {
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
|
||||
.wrn-context-menu[data-placement="pointer"] .wrn-context-menu__panel {
|
||||
position: fixed;
|
||||
top: var(--wire-context-y);
|
||||
left: var(--wire-context-x);
|
||||
top: var(--wrn-context-y);
|
||||
left: var(--wrn-context-x);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="bottom-end"] .wire-context-menu__panel {
|
||||
.wrn-context-menu[data-placement="bottom-end"] .wrn-context-menu__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="top-start"] .wire-context-menu__panel {
|
||||
.wrn-context-menu[data-placement="top-start"] .wrn-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 {
|
||||
.wrn-context-menu[data-placement="top-end"] .wrn-context-menu__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
@@ -359,58 +359,58 @@ component ContextMenu {
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-variant="soft"] .wire-context-menu__panel {
|
||||
.wrn-context-menu[data-variant="soft"] .wrn-context-menu__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--context-soft), transparent 72%),
|
||||
var(--wire-color-surface-raised);
|
||||
var(--wrn-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);
|
||||
.wrn-context-menu[data-variant="outline"] .wrn-context-menu__panel {
|
||||
background: var(--wrn-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__header {
|
||||
.wrn-context-menu__header {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
padding: 0.65rem 0.75rem 0.75rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-context-menu__header strong {
|
||||
.wrn-context-menu__header strong {
|
||||
font-size: 0.86rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wire-context-menu__header span {
|
||||
color: var(--wire-color-text-muted);
|
||||
.wrn-context-menu__header span {
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.wire-context-menu__items {
|
||||
.wrn-context-menu__items {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
padding-block: 0.25rem;
|
||||
}
|
||||
|
||||
.wire-context-menu__section-label {
|
||||
.wrn-context-menu__section-label {
|
||||
padding: 0.55rem 0.75rem 0.3rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-context-menu__divider {
|
||||
.wrn-context-menu__divider {
|
||||
height: 1px;
|
||||
margin: 0.3rem 0.45rem;
|
||||
background: var(--wire-color-border);
|
||||
background: var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-context-menu__item {
|
||||
.wrn-context-menu__item {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
@@ -419,7 +419,7 @@ component ContextMenu {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.65rem 0.7rem;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-color-text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0.72rem;
|
||||
@@ -430,47 +430,47 @@ component ContextMenu {
|
||||
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"] {
|
||||
.wrn-context-menu__item:hover,
|
||||
.wrn-context-menu__item:focus-visible,
|
||||
.wrn-context-menu__item[data-selected="true"] {
|
||||
color: var(--context-accent);
|
||||
background: var(--context-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__item:active {
|
||||
.wrn-context-menu__item:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[data-danger="true"] {
|
||||
color: var(--wire-color-danger);
|
||||
.wrn-context-menu__item[data-danger="true"] {
|
||||
color: var(--wrn-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);
|
||||
.wrn-context-menu__item[data-danger="true"]:hover,
|
||||
.wrn-context-menu__item[data-danger="true"]:focus-visible {
|
||||
background: var(--wrn-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[disabled],
|
||||
.wire-context-menu__item[aria-disabled="true"] {
|
||||
.wrn-context-menu__item[disabled],
|
||||
.wrn-context-menu__item[aria-disabled="true"] {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__icon,
|
||||
.wire-context-menu__status {
|
||||
.wrn-context-menu__icon,
|
||||
.wrn-context-menu__status {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy {
|
||||
.wrn-context-menu__copy {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy strong {
|
||||
.wrn-context-menu__copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
@@ -479,35 +479,35 @@ component ContextMenu {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy small {
|
||||
.wrn-context-menu__copy small {
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text-muted);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-context-menu kbd {
|
||||
.wrn-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);
|
||||
color: var(--wrn-color-text-muted);
|
||||
background: var(--wrn-color-surface-soft);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.36rem;
|
||||
font-size: 0.64rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-size="sm"] .wire-context-menu__item {
|
||||
.wrn-context-menu[data-size="sm"] .wrn-context-menu__item {
|
||||
padding: 0.52rem 0.6rem;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-size="lg"] .wire-context-menu__item {
|
||||
.wrn-context-menu[data-size="lg"] .wrn-context-menu__item {
|
||||
padding: 0.78rem 0.82rem;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
|
||||
.wrn-context-menu[data-placement="pointer"] .wrn-context-menu__panel {
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
@@ -518,7 +518,7 @@ component ContextMenu {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-context-menu__item {
|
||||
.wrn-context-menu__item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,122 @@
|
||||
// CustomScrollbar -- a scrolling region with a themed scrollbar.
|
||||
//
|
||||
// <CustomScrollbar maxHeight="20rem">...long content...</CustomScrollbar>
|
||||
//
|
||||
// Scrollbar appearance is CSS, not script: scrollbar-width and scrollbar-color
|
||||
// are the standard properties, and the ::-webkit-scrollbar rules cover the
|
||||
// browsers that still need them.
|
||||
//
|
||||
// This component used to declare a scroll output it never emitted, and its
|
||||
// props were columns, gap and maxWidth copied from a grid scaffold. The output
|
||||
// is removed rather than left unimplemented -- a caller can listen for a plain
|
||||
// scroll event on the element, which is what it would have been anyway.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
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"
|
||||
size: string = "default"
|
||||
axis: string = "vertical"
|
||||
// Track thickness in pixels. Clamped, because it arrives as an attribute
|
||||
// and a scrollbar wider than the content is not useful to anyone.
|
||||
thickness: number = 8
|
||||
maxHeight: string = "20rem"
|
||||
radius: string = "999px"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
shared function trackSize() {
|
||||
var value = Number(thickness)
|
||||
if (!value || value < 2) {
|
||||
return 8
|
||||
}
|
||||
return Math.min(24, value)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="CustomScrollbar"
|
||||
class='wrn-scrollbar {class}'
|
||||
data-color='{color}'
|
||||
data-size='{size}'
|
||||
data-axis='{axis}'
|
||||
style='--scrollbar-thickness:{trackSize()}px;--scrollbar-radius:{radius};max-height:{maxHeight};'
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-scrollbar {
|
||||
--scrollbar-accent: var(--wrn-color-primary);
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
/*
|
||||
* The standard properties. Firefox and current Chrome honour these; the
|
||||
* webkit rules below are only for the engines that still ignore them.
|
||||
*/
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--scrollbar-accent) 55%, transparent) transparent;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wrn-scrollbar[data-color="secondary"] {
|
||||
--scrollbar-accent: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-scrollbar[data-color="success"] {
|
||||
--scrollbar-accent: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-scrollbar[data-color="danger"] {
|
||||
--scrollbar-accent: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-scrollbar[data-color="info"] {
|
||||
--scrollbar-accent: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-scrollbar[data-axis="vertical"] {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wrn-scrollbar[data-axis="horizontal"] {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.wrn-scrollbar::-webkit-scrollbar {
|
||||
width: var(--scrollbar-thickness, 8px);
|
||||
height: var(--scrollbar-thickness, 8px);
|
||||
}
|
||||
|
||||
.wrn-scrollbar::-webkit-scrollbar-track {
|
||||
background: var(--wrn-color-surface-soft);
|
||||
border-radius: var(--scrollbar-radius, 999px);
|
||||
}
|
||||
|
||||
.wrn-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--scrollbar-accent) 45%, transparent);
|
||||
border-radius: var(--scrollbar-radius, 999px);
|
||||
}
|
||||
|
||||
.wrn-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-accent);
|
||||
}
|
||||
|
||||
/*
|
||||
* A pointer-less device paints its own overlay scrollbar and ignores the
|
||||
* width above, so the region keeps its own padding instead.
|
||||
*/
|
||||
@media (hover: none) {
|
||||
.wrn-scrollbar {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,95 @@ component DataMap {
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--data-map wire-next--variant-{variant} {class}">
|
||||
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--data-map wrn-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}
|
||||
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--data-map {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius);
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--data-map > p { margin: 0; color: var(--wrn-color-muted); }
|
||||
.wrn-next--data-map > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.wrn-next--data-map > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,18 +45,634 @@ size: string = "default"
|
||||
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 selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".wrn-next--date-picker"); input = root.querySelector(".wrn-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(".wrn-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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
|
||||
<div class="wrn-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wrn-next__picker-control"><input id="{id || name}" class="wrn-next__sr-only wrn-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wrn-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="wrn-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
|
||||
<div class="wrn-next__calendar" role="dialog" aria-label="{label}"><div class="wrn-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="wrn-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="wrn-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
|
||||
{#if helperText}<small class="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next__picker-clear {
|
||||
position: absolute;
|
||||
right: 2.5rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--wrn-color-muted);
|
||||
}
|
||||
|
||||
.wrn-next__date-selectors {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.wrn-next__date-selectors label {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wrn-next__date-selectors select {
|
||||
width: 100%;
|
||||
min-height: 2.35rem;
|
||||
padding: 0.45rem 0.65rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.wrn-next__date-done {
|
||||
background: var(--wrn-field-color) !important;
|
||||
color: var(--wrn-color-primary-contrast, #fff) !important;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wrn-next__calendar > header {
|
||||
display: grid;
|
||||
grid-template-columns: 2.25rem 1fr 2.25rem;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrn-next__calendar-grid button[data-outside="true"] {
|
||||
color: var(--wrn-color-muted);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.wrn-next__calendar-grid button[data-today="true"] {
|
||||
box-shadow: inset 0 0 0 1px var(--wrn-field-color);
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Shared field foundation. */
|
||||
|
||||
/* Validation states set by the browser validation runtime. */
|
||||
.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger) !important;
|
||||
}
|
||||
|
||||
.wrn-field-error {
|
||||
display: block;
|
||||
min-height: 1em;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.wrn-next--choice {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next--choice input {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
accent-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-next--field {
|
||||
display: grid;
|
||||
width: min(100%, 28rem);
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select) {
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.6rem;
|
||||
outline: 0;
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-bg);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-auth-form form > .wrn-next--field,
|
||||
.wrn-auth-form form > .wrn-field {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.wrn-auth-form__submit > .wrn-btn {
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select):focus-visible {
|
||||
border-color: var(--wrn-color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
|
||||
}
|
||||
.wrn-next--field textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
.wrn-next--field,
|
||||
.wrn-next--choice-field {
|
||||
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
width: min(100%, 34rem);
|
||||
min-width: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-sm,
|
||||
.wrn-next--choice-field.wrn-next--size-sm {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-lg,
|
||||
.wrn-next--choice-field.wrn-next--size-lg {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading label {
|
||||
color: var(--wrn-color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-hint,
|
||||
.wrn-next__field-help {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.82em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__field-control {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-inline: 0.85rem;
|
||||
border-color: var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
|
||||
padding-inline-start: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
|
||||
padding-inline-end: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
|
||||
right: 0.85rem;
|
||||
left: auto;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
padding-inline: 0;
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
|
||||
border-radius: 999px;
|
||||
padding-inline: 1.15rem;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__field-floating-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
transition: 150ms ease;
|
||||
}
|
||||
.wrn-next--textarea .wrn-next__field-floating-label {
|
||||
top: 1.35rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-top: 1.2rem;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
|
||||
+ .wrn-next__field-floating-label {
|
||||
top: 0.42rem;
|
||||
font-size: 0.7em;
|
||||
transform: none;
|
||||
}
|
||||
.wrn-next__field-error {
|
||||
min-height: 1em;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.82em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-error:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
|
||||
.wrn-next--choice-field[data-invalid="true"] input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):disabled,
|
||||
.wrn-next--choice-field input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):read-only {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
|
||||
display: flex;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
.wrn-next__sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next__choice-field {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.wrn-next__choice {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
color: var(--wrn-color-text);
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__checkbox-content > .wrn-next__choice {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.wrn-next__choice input {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
accent-color: var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next__choice-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.15rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrn-next__choice-copy small {
|
||||
color: var(--wrn-color-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__radio-group,
|
||||
.wrn-next__checkbox-group {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
|
||||
align-items: stretch;
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
|
||||
min-height: 4.5rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
|
||||
border-color: var(--wrn-field-color);
|
||||
box-shadow: 0 0 0 1px var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
|
||||
padding-block: 0.75rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
|
||||
order: 2;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
|
||||
order: 1;
|
||||
}
|
||||
.wrn-next__choice[data-variant="outlined"],
|
||||
.wrn-next__choice[data-variant="floating"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
.wrn-next__choice[data-variant="pill"] {
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__choice[data-variant="ghost"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
|
||||
}
|
||||
.wrn-next__choice[data-variant="underline"] {
|
||||
padding-block: 0.5rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--select .wrn-next__field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__color-control,
|
||||
.wrn-next__range-control,
|
||||
.wrn-next__input-group-control,
|
||||
.wrn-next__file-control {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next__picker-control,
|
||||
.wrn-next--time-picker .wrn-next__field-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__picker-control > button:first-of-type,
|
||||
.wrn-next__time-trigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.wrn-next__calendar,
|
||||
.wrn-next__time-panel {
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
width: min(100%, 22rem);
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-3);
|
||||
}
|
||||
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
|
||||
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next__calendar button,
|
||||
.wrn-next__time-panel button {
|
||||
min-height: 2.25rem;
|
||||
border: 0;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.wrn-next__calendar-weekdays,
|
||||
.wrn-next__calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.wrn-next__calendar-weekdays {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.wrn-next__calendar-grid button:hover,
|
||||
.wrn-next__time-panel button:hover {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next__calendar-grid button[aria-pressed="true"],
|
||||
.wrn-next__time-panel button[aria-pressed="true"] {
|
||||
background: var(--wrn-field-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
}
|
||||
.wrn-next__time-period {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row,
|
||||
.wrn-next__upload-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
|
||||
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
|
||||
}
|
||||
.wrn-next--strong-password.wrn-next--invalid input,
|
||||
.wrn-next--strong-password input.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,151 @@ component DeviceFrame {
|
||||
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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--device-frame wrn-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="wrn-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="wrn-next__device-shell">{#if src || srcdoc}<iframe title="{frameTitle}" src="{src}" srcdoc="{srcdoc}" allow="{allow}"></iframe>{:else}<div class="wrn-next__device-content"><slot /></div>{/if}</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--device-frame {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
}
|
||||
|
||||
.wrn-next--device-frame > header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next__device-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.wrn-next__device-actions button {
|
||||
min-height: 2.25rem;
|
||||
padding-inline: 0.7rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface-2);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wrn-next__device-shell {
|
||||
overflow: hidden;
|
||||
width: min(100%, 24rem);
|
||||
min-height: 28rem;
|
||||
margin-inline: auto;
|
||||
border: 0.65rem solid var(--wrn-color-text);
|
||||
border-radius: 2rem;
|
||||
background: var(--wrn-color-background);
|
||||
transition: width var(--wrn-motion-normal) var(--wrn-ease-standard);
|
||||
}
|
||||
|
||||
.wrn-next--device-frame[data-device="tablet"] .wrn-next__device-shell {
|
||||
width: min(100%, 42rem);
|
||||
}
|
||||
|
||||
.wrn-next--device-frame[data-device="desktop"] .wrn-next__device-shell {
|
||||
width: 100%;
|
||||
border-width: 0.4rem;
|
||||
border-radius: var(--wrn-radius-md);
|
||||
}
|
||||
|
||||
.wrn-next--device-frame[data-orientation="landscape"] .wrn-next__device-shell {
|
||||
width: min(100%, 48rem);
|
||||
min-height: 20rem;
|
||||
}
|
||||
|
||||
.wrn-next__device-shell :is(iframe, .wrn-next__device-content) {
|
||||
width: 100%;
|
||||
min-height: inherit;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next--device-frame > header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,95 @@ component DragAndDrop {
|
||||
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}">
|
||||
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--drag-and-drop wrn-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}
|
||||
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--drag-and-drop {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius);
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--drag-and-drop > p { margin: 0; color: var(--wrn-color-muted); }
|
||||
.wrn-next--drag-and-drop > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.wrn-next--drag-and-drop > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+107
-107
@@ -80,14 +80,14 @@ open: boolean = false
|
||||
data-variant='{variant}'
|
||||
data-overlay='{overlay ? "true" : "false"}'
|
||||
data-scrollable='{scrollable ? "true" : "false"}'
|
||||
class='wire-drawer {class}'
|
||||
class='wrn-drawer {class}'
|
||||
style='--drawer-duration: {duration}ms'
|
||||
>
|
||||
|
||||
{#if triggerLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__trigger"
|
||||
class="wrn-drawer__trigger"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='showDrawer(event)'
|
||||
@@ -99,40 +99,40 @@ open: boolean = false
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span class="wire-drawer__trigger-slot" @click='showDrawer(event)'>
|
||||
<span class="wrn-drawer__trigger-slot" @click='showDrawer(event)'>
|
||||
<slot name="trigger"></slot>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="wire-drawer__layer"
|
||||
class="wrn-drawer__layer"
|
||||
role="presentation"
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
{#if overlay}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__backdrop"
|
||||
class="wrn-drawer__backdrop"
|
||||
aria-label='{closeLabel}'
|
||||
@click='if (closeOnBackdrop) { cancelDrawer(event) }'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="wire-drawer__panel"
|
||||
class="wrn-drawer__panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label='{label || title}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="wire-drawer__handle" aria-hidden="true"></div>
|
||||
<div class="wrn-drawer__handle" aria-hidden="true"></div>
|
||||
|
||||
<header class="wire-drawer__header">
|
||||
<div class="wire-drawer__heading">
|
||||
<header class="wrn-drawer__header">
|
||||
<div class="wrn-drawer__heading">
|
||||
{#if icon}
|
||||
<span class='wire-drawer__icon {icon}' aria-hidden="true"></span>
|
||||
<span class='wrn-drawer__icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<div class="wire-drawer__heading-copy">
|
||||
<div class="wrn-drawer__heading-copy">
|
||||
<slot name="header"></slot>
|
||||
{#if title}
|
||||
<h2>{title}</h2>
|
||||
@@ -146,7 +146,7 @@ open: boolean = false
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__close"
|
||||
class="wrn-drawer__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hideDrawer("close-button", event)'
|
||||
>
|
||||
@@ -155,11 +155,11 @@ open: boolean = false
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="wire-drawer__body">
|
||||
<div class="wrn-drawer__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<footer class="wire-drawer__footer">
|
||||
<footer class="wrn-drawer__footer">
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -168,50 +168,50 @@ open: boolean = false
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-drawer {
|
||||
--drawer-accent: var(--wire-color-primary);
|
||||
--drawer-soft: var(--wire-color-primary-soft);
|
||||
.wrn-drawer {
|
||||
--drawer-accent: var(--wrn-color-primary);
|
||||
--drawer-soft: var(--wrn-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);
|
||||
.wrn-drawer[data-color="secondary"] {
|
||||
--drawer-accent: var(--wrn-color-secondary);
|
||||
--drawer-soft: var(--wrn-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="info"] {
|
||||
--drawer-accent: var(--wire-color-info);
|
||||
--drawer-soft: var(--wire-color-info-soft);
|
||||
.wrn-drawer[data-color="info"] {
|
||||
--drawer-accent: var(--wrn-color-info);
|
||||
--drawer-soft: var(--wrn-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="success"] {
|
||||
--drawer-accent: var(--wire-color-success);
|
||||
--drawer-soft: var(--wire-color-success-soft);
|
||||
.wrn-drawer[data-color="success"] {
|
||||
--drawer-accent: var(--wrn-color-success);
|
||||
--drawer-soft: var(--wrn-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="warning"] {
|
||||
--drawer-accent: var(--wire-color-warning);
|
||||
--drawer-soft: var(--wire-color-warning-soft);
|
||||
.wrn-drawer[data-color="warning"] {
|
||||
--drawer-accent: var(--wrn-color-warning);
|
||||
--drawer-soft: var(--wrn-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="danger"] {
|
||||
--drawer-accent: var(--wire-color-danger);
|
||||
--drawer-soft: var(--wire-color-danger-soft);
|
||||
.wrn-drawer[data-color="danger"] {
|
||||
--drawer-accent: var(--wrn-color-danger);
|
||||
--drawer-soft: var(--wrn-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-drawer__trigger,
|
||||
.wire-drawer__trigger-slot {
|
||||
.wrn-drawer__trigger,
|
||||
.wrn-drawer__trigger-slot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.wire-drawer__trigger {
|
||||
.wrn-drawer__trigger {
|
||||
appearance: none;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
color: var(--wire-color-primary-contrast);
|
||||
color: var(--wrn-color-primary-contrast);
|
||||
background: var(--drawer-accent);
|
||||
border: 0;
|
||||
border-radius: 0.8rem;
|
||||
@@ -228,7 +228,7 @@ open: boolean = false
|
||||
* delayed by the duration on the way out so the panel can finish sliding
|
||||
* before the layer is taken out of the hit-testing tree.
|
||||
*/
|
||||
.wire-drawer__layer {
|
||||
.wrn-drawer__layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
@@ -241,7 +241,7 @@ open: boolean = false
|
||||
visibility 0s linear var(--drawer-duration, 260ms);
|
||||
}
|
||||
|
||||
.wire-drawer[data-open="true"] .wire-drawer__layer {
|
||||
.wrn-drawer[data-open="true"] .wrn-drawer__layer {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition:
|
||||
@@ -249,7 +249,7 @@ open: boolean = false
|
||||
visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.wire-drawer__backdrop {
|
||||
.wrn-drawer__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
@@ -261,11 +261,11 @@ open: boolean = false
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.wire-drawer[data-overlay="false"] .wire-drawer__backdrop {
|
||||
.wrn-drawer[data-overlay="false"] .wrn-drawer__backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-drawer__panel {
|
||||
.wrn-drawer__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
@@ -273,15 +273,15 @@ open: boolean = false
|
||||
width: min(28rem, calc(100vw - 1rem));
|
||||
max-height: 100%;
|
||||
margin-left: auto;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-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));
|
||||
var(--wrn-color-surface-raised);
|
||||
border-left: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-color-border));
|
||||
box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
@@ -290,66 +290,66 @@ open: boolean = false
|
||||
transition: transform var(--drawer-duration, 260ms) cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
.wire-drawer[data-open="true"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-open="true"] .wrn-drawer__panel {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="left"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-placement="left"] .wrn-drawer__panel {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-placement="top"] .wrn-drawer__panel {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-placement="bottom"] .wrn-drawer__panel {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.wire-drawer[data-open="true"][data-placement="left"] .wire-drawer__panel,
|
||||
.wire-drawer[data-open="true"][data-placement="top"] .wire-drawer__panel,
|
||||
.wire-drawer[data-open="true"][data-placement="bottom"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-open="true"][data-placement="left"] .wrn-drawer__panel,
|
||||
.wrn-drawer[data-open="true"][data-placement="top"] .wrn-drawer__panel,
|
||||
.wrn-drawer[data-open="true"][data-placement="bottom"] .wrn-drawer__panel {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-drawer__layer,
|
||||
.wire-drawer__panel {
|
||||
.wrn-drawer__layer,
|
||||
.wrn-drawer__panel {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="sm"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-size="sm"] .wrn-drawer__panel {
|
||||
width: min(22rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="lg"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-size="lg"] .wrn-drawer__panel {
|
||||
width: min(38rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="xl"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-size="xl"] .wrn-drawer__panel {
|
||||
width: min(52rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="full"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-size="full"] .wrn-drawer__panel {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="left"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-placement="left"] .wrn-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-right: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-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 {
|
||||
.wrn-drawer[data-placement="top"] .wrn-drawer__layer,
|
||||
.wrn-drawer[data-placement="bottom"] .wrn-drawer__layer {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__panel,
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-placement="top"] .wrn-drawer__panel,
|
||||
.wrn-drawer[data-placement="bottom"] .wrn-drawer__panel {
|
||||
width: 100%;
|
||||
max-height: min(80vh, 44rem);
|
||||
margin: 0;
|
||||
@@ -357,61 +357,61 @@ open: boolean = false
|
||||
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));
|
||||
.wrn-drawer[data-placement="top"] .wrn-drawer__panel {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-color-border));
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
|
||||
.wrn-drawer[data-placement="bottom"] .wrn-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));
|
||||
.wrn-drawer[data-placement="bottom"] .wrn-drawer__panel {
|
||||
border-top: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-color-border));
|
||||
border-radius: 1.3rem 1.3rem 0 0;
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="soft"] .wire-drawer__panel {
|
||||
.wrn-drawer[data-variant="soft"] .wrn-drawer__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--drawer-soft), transparent 65%),
|
||||
var(--wire-color-surface-raised);
|
||||
var(--wrn-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__panel {
|
||||
color: var(--wire-color-primary-contrast);
|
||||
.wrn-drawer[data-variant="solid"] .wrn-drawer__panel {
|
||||
color: var(--wrn-color-primary-contrast);
|
||||
background: var(--drawer-accent);
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer__handle {
|
||||
.wrn-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);
|
||||
background: color-mix(in srgb, var(--wrn-color-text-muted) 48%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__handle {
|
||||
.wrn-drawer[data-placement="bottom"] .wrn-drawer__handle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wire-drawer__header {
|
||||
.wrn-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);
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-drawer__heading {
|
||||
.wrn-drawer__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.85rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__icon {
|
||||
.wrn-drawer__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
@@ -419,34 +419,34 @@ open: boolean = false
|
||||
color: var(--drawer-accent);
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__icon {
|
||||
.wrn-drawer[data-variant="solid"] .wrn-drawer__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy {
|
||||
.wrn-drawer__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy h2,
|
||||
.wire-drawer__heading-copy p {
|
||||
.wrn-drawer__heading-copy h2,
|
||||
.wrn-drawer__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy h2 {
|
||||
.wrn-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);
|
||||
.wrn-drawer__heading-copy p {
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__heading-copy p {
|
||||
.wrn-drawer[data-variant="solid"] .wrn-drawer__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ open: boolean = false
|
||||
* content box of a couple of pixels, which squeezes the icon to a sliver
|
||||
* and reads as "the close button has no icon". Same trap as Modal.
|
||||
*/
|
||||
.wire-drawer__close {
|
||||
.wrn-drawer__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -465,22 +465,22 @@ open: boolean = false
|
||||
padding: 0;
|
||||
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);
|
||||
color: var(--wrn-color-text-muted);
|
||||
background: var(--wrn-color-surface-soft);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-drawer__close:hover,
|
||||
.wire-drawer__close:focus-visible {
|
||||
.wrn-drawer__close:hover,
|
||||
.wrn-drawer__close:focus-visible {
|
||||
color: var(--drawer-accent);
|
||||
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wire-color-border));
|
||||
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wrn-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Never let the glyph be shrunk by the flex container. */
|
||||
.wire-drawer__close svg {
|
||||
.wrn-drawer__close svg {
|
||||
flex: 0 0 auto;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@@ -494,40 +494,40 @@ open: boolean = false
|
||||
* :where() keeps the specificity low enough that any class the app puts on
|
||||
* its own slot content still wins.
|
||||
*/
|
||||
.wire-drawer__body {
|
||||
.wrn-drawer__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 1.35rem;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wire-drawer__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
|
||||
.wrn-drawer__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wire-drawer[data-scrollable="true"] .wire-drawer__body {
|
||||
.wrn-drawer[data-scrollable="true"] .wrn-drawer__body {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wire-drawer__footer {
|
||||
.wrn-drawer__footer {
|
||||
padding: 1rem 1.35rem 1.35rem;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
border-top: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-drawer__footer:empty {
|
||||
.wrn-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 {
|
||||
.wrn-drawer[data-placement="right"] .wrn-drawer__panel,
|
||||
.wrn-drawer[data-placement="left"] .wrn-drawer__panel {
|
||||
width: min(24rem, 100vw);
|
||||
}
|
||||
|
||||
.wire-drawer__header,
|
||||
.wire-drawer__body,
|
||||
.wire-drawer__footer {
|
||||
.wrn-drawer__header,
|
||||
.wrn-drawer__body,
|
||||
.wrn-drawer__footer {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
+109
-109
@@ -82,7 +82,7 @@ items: unknown[] = []
|
||||
}
|
||||
|
||||
client function moveFocus(sourceEvent, direction) {
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
|
||||
if (!options.length) {
|
||||
return
|
||||
@@ -110,14 +110,14 @@ items: unknown[] = []
|
||||
moveFocus(sourceEvent, -1)
|
||||
} else if (sourceEvent.key === "Home" && isOpen()) {
|
||||
sourceEvent.preventDefault()
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const root = sourceEvent.currentTarget.closest(".wrn-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 root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
|
||||
if (options.length) {
|
||||
options[options.length - 1].focus()
|
||||
@@ -136,12 +136,12 @@ items: unknown[] = []
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='wire-dropdown {class}'
|
||||
class='wrn-dropdown {class}'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-dropdown__trigger"
|
||||
class="wrn-dropdown__trigger"
|
||||
disabled='{disabled}'
|
||||
aria-haspopup="menu"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@@ -149,20 +149,20 @@ items: unknown[] = []
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
{#if icon}
|
||||
<span class='wire-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
|
||||
<span class='wrn-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if label}
|
||||
<span class="wire-dropdown__trigger-label">{label}</span>
|
||||
<span class="wrn-dropdown__trigger-label">{label}</span>
|
||||
{/if}
|
||||
{#if showChevron}
|
||||
<span class="icon-[lucide--chevron-down] wire-dropdown__chevron" aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--chevron-down] wrn-dropdown__chevron" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-dropdown__dismiss-layer"
|
||||
class="wrn-dropdown__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label="Close menu"
|
||||
@click='hideMenu("outside", event)'
|
||||
@@ -170,7 +170,7 @@ items: unknown[] = []
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-dropdown__panel"
|
||||
class="wrn-dropdown__panel"
|
||||
data-wrn-anchored="true"
|
||||
data-show='{open || visible}'
|
||||
role="menu"
|
||||
@@ -178,12 +178,12 @@ items: unknown[] = []
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div class="wire-dropdown__items">
|
||||
<div class="wrn-dropdown__items">
|
||||
{#each items as item, itemIndex}
|
||||
{#if item.type === "divider"}
|
||||
<div class="wire-dropdown__divider" role="separator"></div>
|
||||
<div class="wrn-dropdown__divider" role="separator"></div>
|
||||
{:else if item.type === "header"}
|
||||
<div class="wire-dropdown__section-label">{item.label || item.title}</div>
|
||||
<div class="wrn-dropdown__section-label">{item.label || item.title}</div>
|
||||
{:else if item.href}
|
||||
<a
|
||||
href='{item.href}'
|
||||
@@ -194,26 +194,26 @@ items: unknown[] = []
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
class="wire-dropdown__item"
|
||||
class="wrn-dropdown__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
<span class='wrn-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-dropdown__copy">
|
||||
<span class="wrn-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>
|
||||
<span class="wrn-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>
|
||||
<span class="icon-[lucide--check] wrn-dropdown__status" aria-hidden="true"></span>
|
||||
{:else if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-dropdown__status" aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--arrow-up-right] wrn-dropdown__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
@@ -224,29 +224,29 @@ items: unknown[] = []
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
disabled='{item.disabled}'
|
||||
class="wire-dropdown__item"
|
||||
class="wrn-dropdown__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
<span class='wrn-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-dropdown__copy">
|
||||
<span class="wrn-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>
|
||||
<span class="wrn-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>
|
||||
<span class="icon-[lucide--check] wrn-dropdown__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{:empty}
|
||||
<div class="wire-dropdown__empty">{emptyLabel}</div>
|
||||
<div class="wrn-dropdown__empty">{emptyLabel}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -257,40 +257,40 @@ items: unknown[] = []
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-dropdown {
|
||||
--dropdown-accent: var(--wire-color-primary);
|
||||
--dropdown-soft: var(--wire-color-primary-soft);
|
||||
.wrn-dropdown {
|
||||
--dropdown-accent: var(--wrn-color-primary);
|
||||
--dropdown-soft: var(--wrn-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);
|
||||
.wrn-dropdown[data-color="secondary"] {
|
||||
--dropdown-accent: var(--wrn-color-secondary);
|
||||
--dropdown-soft: var(--wrn-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="info"] {
|
||||
--dropdown-accent: var(--wire-color-info);
|
||||
--dropdown-soft: var(--wire-color-info-soft);
|
||||
.wrn-dropdown[data-color="info"] {
|
||||
--dropdown-accent: var(--wrn-color-info);
|
||||
--dropdown-soft: var(--wrn-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="success"] {
|
||||
--dropdown-accent: var(--wire-color-success);
|
||||
--dropdown-soft: var(--wire-color-success-soft);
|
||||
.wrn-dropdown[data-color="success"] {
|
||||
--dropdown-accent: var(--wrn-color-success);
|
||||
--dropdown-soft: var(--wrn-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="warning"] {
|
||||
--dropdown-accent: var(--wire-color-warning);
|
||||
--dropdown-soft: var(--wire-color-warning-soft);
|
||||
.wrn-dropdown[data-color="warning"] {
|
||||
--dropdown-accent: var(--wrn-color-warning);
|
||||
--dropdown-soft: var(--wrn-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="danger"] {
|
||||
--dropdown-accent: var(--wire-color-danger);
|
||||
--dropdown-soft: var(--wire-color-danger-soft);
|
||||
.wrn-dropdown[data-color="danger"] {
|
||||
--dropdown-accent: var(--wrn-color-danger);
|
||||
--dropdown-soft: var(--wrn-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger {
|
||||
.wrn-dropdown__trigger {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -298,9 +298,9 @@ items: unknown[] = []
|
||||
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);
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface-raised);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.78rem;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
@@ -309,47 +309,47 @@ items: unknown[] = []
|
||||
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 {
|
||||
.wrn-dropdown__trigger:hover,
|
||||
.wrn-dropdown__trigger:focus-visible,
|
||||
.wrn-dropdown[data-open="true"] .wrn-dropdown__trigger {
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border));
|
||||
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wrn-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger:disabled {
|
||||
.wrn-dropdown__trigger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger-icon,
|
||||
.wire-dropdown__chevron {
|
||||
.wrn-dropdown__trigger-icon,
|
||||
.wrn-dropdown__chevron {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__chevron {
|
||||
.wrn-dropdown__chevron {
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-open="true"] .wire-dropdown__chevron {
|
||||
.wrn-dropdown[data-open="true"] .wrn-dropdown__chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-size="sm"] .wire-dropdown__trigger {
|
||||
.wrn-dropdown[data-size="sm"] .wrn-dropdown__trigger {
|
||||
min-height: 2.2rem;
|
||||
padding: 0.5rem 0.72rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-size="lg"] .wire-dropdown__trigger {
|
||||
.wrn-dropdown[data-size="lg"] .wrn-dropdown__trigger {
|
||||
min-height: 2.9rem;
|
||||
padding: 0.78rem 1.05rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__dismiss-layer {
|
||||
.wrn-dropdown__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
@@ -359,7 +359,7 @@ items: unknown[] = []
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown__panel {
|
||||
.wrn-dropdown__panel {
|
||||
position: absolute;
|
||||
z-index: 1101;
|
||||
top: calc(100% + 0.55rem);
|
||||
@@ -368,11 +368,11 @@ items: unknown[] = []
|
||||
min-width: 12rem;
|
||||
max-width: min(22rem, calc(100vw - 1.5rem));
|
||||
padding: 0.45rem;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-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));
|
||||
color-mix(in srgb, var(--wrn-color-surface-raised) 97%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wrn-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
@@ -380,73 +380,73 @@ items: unknown[] = []
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="trigger"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-width="trigger"] .wrn-dropdown__panel {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="sm"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-width="sm"] .wrn-dropdown__panel {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="md"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-width="md"] .wrn-dropdown__panel {
|
||||
width: 17rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="lg"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-width="lg"] .wrn-dropdown__panel {
|
||||
width: 22rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-placement="bottom-end"] .wrn-dropdown__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="top-start"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-placement="top-start"] .wrn-dropdown__panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.55rem);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="top-end"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-placement="top-end"] .wrn-dropdown__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.55rem);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-variant="soft"] .wire-dropdown__panel {
|
||||
.wrn-dropdown[data-variant="soft"] .wrn-dropdown__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
|
||||
var(--wire-color-surface-raised);
|
||||
var(--wrn-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);
|
||||
.wrn-dropdown[data-variant="outline"] .wrn-dropdown__panel {
|
||||
background: var(--wrn-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__items {
|
||||
.wrn-dropdown__items {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__section-label {
|
||||
.wrn-dropdown__section-label {
|
||||
padding: 0.55rem 0.72rem 0.3rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-dropdown__divider {
|
||||
.wrn-dropdown__divider {
|
||||
height: 1px;
|
||||
margin: 0.3rem 0.4rem;
|
||||
background: var(--wire-color-border);
|
||||
background: var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-dropdown__item {
|
||||
.wrn-dropdown__item {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
@@ -455,7 +455,7 @@ items: unknown[] = []
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.68rem 0.72rem;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-color-text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0.72rem;
|
||||
@@ -466,46 +466,46 @@ items: unknown[] = []
|
||||
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"] {
|
||||
.wrn-dropdown__item:hover,
|
||||
.wrn-dropdown__item:focus-visible,
|
||||
.wrn-dropdown__item[data-selected="true"] {
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__item:active {
|
||||
.wrn-dropdown__item:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[data-danger="true"] {
|
||||
color: var(--wire-color-danger);
|
||||
.wrn-dropdown__item[data-danger="true"] {
|
||||
color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[data-danger="true"]:hover,
|
||||
.wire-dropdown__item[data-danger="true"]:focus-visible {
|
||||
background: var(--wire-color-danger-soft);
|
||||
.wrn-dropdown__item[data-danger="true"]:hover,
|
||||
.wrn-dropdown__item[data-danger="true"]:focus-visible {
|
||||
background: var(--wrn-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[disabled],
|
||||
.wire-dropdown__item[aria-disabled="true"] {
|
||||
.wrn-dropdown__item[disabled],
|
||||
.wrn-dropdown__item[aria-disabled="true"] {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__item-icon,
|
||||
.wire-dropdown__status {
|
||||
.wrn-dropdown__item-icon,
|
||||
.wrn-dropdown__status {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy {
|
||||
.wrn-dropdown__copy {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy strong {
|
||||
.wrn-dropdown__copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
@@ -514,16 +514,16 @@ items: unknown[] = []
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy small {
|
||||
.wrn-dropdown__copy small {
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text-muted);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-dropdown__badge {
|
||||
.wrn-dropdown__badge {
|
||||
padding: 0.18rem 0.45rem;
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
@@ -532,25 +532,25 @@ items: unknown[] = []
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-dropdown kbd {
|
||||
.wrn-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);
|
||||
color: var(--wrn-color-text-muted);
|
||||
background: var(--wrn-color-surface-soft);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.36rem;
|
||||
font-size: 0.64rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.wire-dropdown__empty {
|
||||
.wrn-dropdown__empty {
|
||||
padding: 1rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-dropdown__panel {
|
||||
.wrn-dropdown__panel {
|
||||
position: fixed;
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
@@ -563,9 +563,9 @@ items: unknown[] = []
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-dropdown__trigger,
|
||||
.wire-dropdown__chevron,
|
||||
.wire-dropdown__item {
|
||||
.wrn-dropdown__trigger,
|
||||
.wrn-dropdown__chevron,
|
||||
.wrn-dropdown__item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,587 @@ size: string = "default"
|
||||
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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--file-input {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
|
||||
<div class="wrn-next__field-heading"><label class="{hiddenLabel ? 'wrn-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wrn-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="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next__file-control {
|
||||
position: relative;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border: 1px dashed var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
|
||||
.wrn-next__file-control input {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Shared field foundation. */
|
||||
|
||||
/* Validation states set by the browser validation runtime. */
|
||||
.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger) !important;
|
||||
}
|
||||
|
||||
.wrn-field-error {
|
||||
display: block;
|
||||
min-height: 1em;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.wrn-next--choice {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next--choice input {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
accent-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-next--field {
|
||||
display: grid;
|
||||
width: min(100%, 28rem);
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select) {
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.6rem;
|
||||
outline: 0;
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-bg);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-auth-form form > .wrn-next--field,
|
||||
.wrn-auth-form form > .wrn-field {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.wrn-auth-form__submit > .wrn-btn {
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select):focus-visible {
|
||||
border-color: var(--wrn-color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
|
||||
}
|
||||
.wrn-next--field textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
.wrn-next--field,
|
||||
.wrn-next--choice-field {
|
||||
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
width: min(100%, 34rem);
|
||||
min-width: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-sm,
|
||||
.wrn-next--choice-field.wrn-next--size-sm {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-lg,
|
||||
.wrn-next--choice-field.wrn-next--size-lg {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading label {
|
||||
color: var(--wrn-color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-hint,
|
||||
.wrn-next__field-help {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.82em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__field-control {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-inline: 0.85rem;
|
||||
border-color: var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
|
||||
padding-inline-start: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
|
||||
padding-inline-end: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
|
||||
right: 0.85rem;
|
||||
left: auto;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
padding-inline: 0;
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
|
||||
border-radius: 999px;
|
||||
padding-inline: 1.15rem;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__field-floating-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
transition: 150ms ease;
|
||||
}
|
||||
.wrn-next--textarea .wrn-next__field-floating-label {
|
||||
top: 1.35rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-top: 1.2rem;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
|
||||
+ .wrn-next__field-floating-label {
|
||||
top: 0.42rem;
|
||||
font-size: 0.7em;
|
||||
transform: none;
|
||||
}
|
||||
.wrn-next__field-error {
|
||||
min-height: 1em;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.82em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-error:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
|
||||
.wrn-next--choice-field[data-invalid="true"] input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):disabled,
|
||||
.wrn-next--choice-field input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):read-only {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
|
||||
display: flex;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
.wrn-next__sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next__choice-field {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.wrn-next__choice {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
color: var(--wrn-color-text);
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__checkbox-content > .wrn-next__choice {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.wrn-next__choice input {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
accent-color: var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next__choice-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.15rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrn-next__choice-copy small {
|
||||
color: var(--wrn-color-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__radio-group,
|
||||
.wrn-next__checkbox-group {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
|
||||
align-items: stretch;
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
|
||||
min-height: 4.5rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
|
||||
border-color: var(--wrn-field-color);
|
||||
box-shadow: 0 0 0 1px var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
|
||||
padding-block: 0.75rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
|
||||
order: 2;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
|
||||
order: 1;
|
||||
}
|
||||
.wrn-next__choice[data-variant="outlined"],
|
||||
.wrn-next__choice[data-variant="floating"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
.wrn-next__choice[data-variant="pill"] {
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__choice[data-variant="ghost"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
|
||||
}
|
||||
.wrn-next__choice[data-variant="underline"] {
|
||||
padding-block: 0.5rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--select .wrn-next__field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__color-control,
|
||||
.wrn-next__range-control,
|
||||
.wrn-next__input-group-control,
|
||||
.wrn-next__file-control {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next__picker-control,
|
||||
.wrn-next--time-picker .wrn-next__field-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__picker-control > button:first-of-type,
|
||||
.wrn-next__time-trigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.wrn-next__calendar,
|
||||
.wrn-next__time-panel {
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
width: min(100%, 22rem);
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-3);
|
||||
}
|
||||
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
|
||||
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next__calendar button,
|
||||
.wrn-next__time-panel button {
|
||||
min-height: 2.25rem;
|
||||
border: 0;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.wrn-next__calendar-weekdays,
|
||||
.wrn-next__calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.wrn-next__calendar-weekdays {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.wrn-next__calendar-grid button:hover,
|
||||
.wrn-next__time-panel button:hover {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next__calendar-grid button[aria-pressed="true"],
|
||||
.wrn-next__time-panel button[aria-pressed="true"] {
|
||||
background: var(--wrn-field-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
}
|
||||
.wrn-next__time-period {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row,
|
||||
.wrn-next__upload-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
|
||||
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
|
||||
}
|
||||
.wrn-next--strong-password.wrn-next--invalid input,
|
||||
.wrn-next--strong-password input.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,617 @@ size: string = "default"
|
||||
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>
|
||||
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--file-upload-progress {class}" data-status="{currentStatus}" aria-live="polite">
|
||||
<div class="wrn-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 class="wrn-next__upload-status"><span class="wrn-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="wrn-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>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.wrn-next--file-upload-progress progress {
|
||||
width: 100%;
|
||||
accent-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-next--file-upload-progress {
|
||||
display: grid;
|
||||
width: min(100%, 36rem);
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
}
|
||||
|
||||
.wrn-next--file-upload-progress progress {
|
||||
width: 100%;
|
||||
height: 0.55rem;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
accent-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-next__upload-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wrn-next__upload-actions button {
|
||||
min-height: 2.25rem;
|
||||
padding-inline: 0.8rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface-2);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Shared field foundation. */
|
||||
|
||||
/* Validation states set by the browser validation runtime. */
|
||||
.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger) !important;
|
||||
}
|
||||
|
||||
.wrn-field-error {
|
||||
display: block;
|
||||
min-height: 1em;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.wrn-next--choice {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next--choice input {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
accent-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-next--field {
|
||||
display: grid;
|
||||
width: min(100%, 28rem);
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select) {
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.6rem;
|
||||
outline: 0;
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-bg);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-auth-form form > .wrn-next--field,
|
||||
.wrn-auth-form form > .wrn-field {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.wrn-auth-form__submit > .wrn-btn {
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select):focus-visible {
|
||||
border-color: var(--wrn-color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
|
||||
}
|
||||
.wrn-next--field textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
.wrn-next--field,
|
||||
.wrn-next--choice-field {
|
||||
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
width: min(100%, 34rem);
|
||||
min-width: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-sm,
|
||||
.wrn-next--choice-field.wrn-next--size-sm {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-lg,
|
||||
.wrn-next--choice-field.wrn-next--size-lg {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading label {
|
||||
color: var(--wrn-color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-hint,
|
||||
.wrn-next__field-help {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.82em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__field-control {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-inline: 0.85rem;
|
||||
border-color: var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
|
||||
padding-inline-start: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
|
||||
padding-inline-end: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
|
||||
right: 0.85rem;
|
||||
left: auto;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
padding-inline: 0;
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
|
||||
border-radius: 999px;
|
||||
padding-inline: 1.15rem;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__field-floating-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
transition: 150ms ease;
|
||||
}
|
||||
.wrn-next--textarea .wrn-next__field-floating-label {
|
||||
top: 1.35rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-top: 1.2rem;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
|
||||
+ .wrn-next__field-floating-label {
|
||||
top: 0.42rem;
|
||||
font-size: 0.7em;
|
||||
transform: none;
|
||||
}
|
||||
.wrn-next__field-error {
|
||||
min-height: 1em;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.82em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-error:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
|
||||
.wrn-next--choice-field[data-invalid="true"] input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):disabled,
|
||||
.wrn-next--choice-field input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):read-only {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
|
||||
display: flex;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
.wrn-next__sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next__choice-field {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.wrn-next__choice {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
color: var(--wrn-color-text);
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__checkbox-content > .wrn-next__choice {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.wrn-next__choice input {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
accent-color: var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next__choice-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.15rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrn-next__choice-copy small {
|
||||
color: var(--wrn-color-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__radio-group,
|
||||
.wrn-next__checkbox-group {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
|
||||
align-items: stretch;
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
|
||||
min-height: 4.5rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
|
||||
border-color: var(--wrn-field-color);
|
||||
box-shadow: 0 0 0 1px var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
|
||||
padding-block: 0.75rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
|
||||
order: 2;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
|
||||
order: 1;
|
||||
}
|
||||
.wrn-next__choice[data-variant="outlined"],
|
||||
.wrn-next__choice[data-variant="floating"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
.wrn-next__choice[data-variant="pill"] {
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__choice[data-variant="ghost"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
|
||||
}
|
||||
.wrn-next__choice[data-variant="underline"] {
|
||||
padding-block: 0.5rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--select .wrn-next__field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__color-control,
|
||||
.wrn-next__range-control,
|
||||
.wrn-next__input-group-control,
|
||||
.wrn-next__file-control {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next__picker-control,
|
||||
.wrn-next--time-picker .wrn-next__field-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__picker-control > button:first-of-type,
|
||||
.wrn-next__time-trigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.wrn-next__calendar,
|
||||
.wrn-next__time-panel {
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
width: min(100%, 22rem);
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-3);
|
||||
}
|
||||
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
|
||||
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next__calendar button,
|
||||
.wrn-next__time-panel button {
|
||||
min-height: 2.25rem;
|
||||
border: 0;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.wrn-next__calendar-weekdays,
|
||||
.wrn-next__calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.wrn-next__calendar-weekdays {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.wrn-next__calendar-grid button:hover,
|
||||
.wrn-next__time-panel button:hover {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next__calendar-grid button[aria-pressed="true"],
|
||||
.wrn-next__time-panel button[aria-pressed="true"] {
|
||||
background: var(--wrn-field-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
}
|
||||
.wrn-next__time-period {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row,
|
||||
.wrn-next__upload-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
|
||||
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
|
||||
}
|
||||
.wrn-next--strong-password.wrn-next--invalid input,
|
||||
.wrn-next--strong-password input.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// Image -- a framed image with a reserved aspect ratio.
|
||||
//
|
||||
// <Image src="/hero.jpg" alt="Dashboard" size="video" rounded={true} />
|
||||
//
|
||||
// The frame keeps its ratio whether or not the image has loaded, so the page
|
||||
// does not jump when it arrives. Without a src it renders a labelled
|
||||
// placeholder of the same shape rather than collapsing.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
component Image {
|
||||
props {
|
||||
size: string = "default"
|
||||
@@ -7,41 +17,111 @@ component Image {
|
||||
width: string = ""
|
||||
height: string = ""
|
||||
loading: string = "lazy"
|
||||
fit: string = "cover"
|
||||
rounded: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<figure
|
||||
{...attrs}
|
||||
data-ui-component="Image"
|
||||
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
|
||||
class:rounded-2xl='rounded'
|
||||
class='wrn-image {class}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-fit='{fit}'
|
||||
data-rounded='{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}
|
||||
<img
|
||||
class="wrn-image__img"
|
||||
data-show="src"
|
||||
src='{src}'
|
||||
alt='{alt}'
|
||||
width='{width}'
|
||||
height='{height}'
|
||||
loading='{loading}'
|
||||
decoding="async"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="wrn-image__placeholder"
|
||||
data-show="!src"
|
||||
role="img"
|
||||
aria-label='{alt || "Image placeholder"}'
|
||||
>
|
||||
<span class="icon-[lucide--image] wrn-image__placeholder-icon" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</figure>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-image {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
background: var(--wrn-color-surface-soft);
|
||||
}
|
||||
|
||||
.wrn-image[data-rounded="true"] {
|
||||
border-radius: var(--wrn-radius-lg);
|
||||
}
|
||||
|
||||
/*
|
||||
* The ratio sits on the frame rather than the image, so the space is
|
||||
* reserved before the file arrives and the page does not jump.
|
||||
*/
|
||||
.wrn-image[data-size="square"] {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.wrn-image[data-size="video"] {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.wrn-image[data-size="landscape"] {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.wrn-image[data-size="portrait"] {
|
||||
aspect-ratio: 3 / 4;
|
||||
}
|
||||
|
||||
.wrn-image__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wrn-image[data-fit="contain"] .wrn-image__img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.wrn-image[data-fit="fill"] .wrn-image__img {
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.wrn-image[data-size="default"] .wrn-image__img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.wrn-image__placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 12rem;
|
||||
height: 100%;
|
||||
color: var(--wrn-color-text-muted);
|
||||
}
|
||||
|
||||
.wrn-image__placeholder-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,16 +39,607 @@ component InputGroup {
|
||||
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}
|
||||
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--input-group {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
|
||||
<div class="wrn-next__field-heading"><label class="{hiddenLabel ? 'wrn-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wrn-next__input-group-control">
|
||||
{#if startText}<span class="wrn-next__input-addon">{startText}</span>{/if}
|
||||
{#if icon}<span class="{icon} wrn-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 endText}<span class="wrn-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>
|
||||
{#if helperText}<small class="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next__input-group-control {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
|
||||
.wrn-next__input-group-control input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wrn-next__input-addon,
|
||||
.wrn-next__input-group-control button {
|
||||
padding: 0.7rem 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wrn-next__input-group-control button {
|
||||
align-self: stretch;
|
||||
border: 0;
|
||||
background: var(--wrn-field-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Shared field foundation. */
|
||||
|
||||
/* Validation states set by the browser validation runtime. */
|
||||
.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger) !important;
|
||||
}
|
||||
|
||||
.wrn-field-error {
|
||||
display: block;
|
||||
min-height: 1em;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.wrn-next--choice {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-next--choice input {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
accent-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-next--field {
|
||||
display: grid;
|
||||
width: min(100%, 28rem);
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select) {
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.6rem;
|
||||
outline: 0;
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-bg);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-auth-form form > .wrn-next--field,
|
||||
.wrn-auth-form form > .wrn-field {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.wrn-auth-form__submit > .wrn-btn {
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
|
||||
}
|
||||
.wrn-next--field :where(input, textarea, select):focus-visible {
|
||||
border-color: var(--wrn-color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
|
||||
}
|
||||
.wrn-next--field textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
.wrn-next--field,
|
||||
.wrn-next--choice-field {
|
||||
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
width: min(100%, 34rem);
|
||||
min-width: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-sm,
|
||||
.wrn-next--choice-field.wrn-next--size-sm {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next--field.wrn-next--size-lg,
|
||||
.wrn-next--choice-field.wrn-next--size-lg {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wrn-next__field-heading label {
|
||||
color: var(--wrn-color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-hint,
|
||||
.wrn-next__field-help {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.82em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__field-control {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-inline: 0.85rem;
|
||||
border-color: var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
|
||||
padding-inline-start: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
|
||||
padding-inline-end: 2.5rem;
|
||||
}
|
||||
.wrn-next__field-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
|
||||
right: 0.85rem;
|
||||
left: auto;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea, select) {
|
||||
padding-inline: 0;
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
|
||||
border-radius: 999px;
|
||||
padding-inline: 1.15rem;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__field-floating-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.85rem;
|
||||
color: var(--wrn-color-muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
transition: 150ms ease;
|
||||
}
|
||||
.wrn-next--textarea .wrn-next__field-floating-label {
|
||||
top: 1.35rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
|
||||
padding-top: 1.2rem;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-floating="true"]
|
||||
.wrn-next__field-control
|
||||
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
|
||||
+ .wrn-next__field-floating-label {
|
||||
top: 0.42rem;
|
||||
font-size: 0.7em;
|
||||
transform: none;
|
||||
}
|
||||
.wrn-next__field-error {
|
||||
min-height: 1em;
|
||||
color: var(--wrn-color-danger);
|
||||
font-size: 0.82em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wrn-next__field-error:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
|
||||
.wrn-next--choice-field[data-invalid="true"] input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):disabled,
|
||||
.wrn-next--choice-field input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
.wrn-next--field :is(input, textarea, select):read-only {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
|
||||
display: flex;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
|
||||
}
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
.wrn-next__sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
margin: -1px !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.wrn-next__choice-field {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.wrn-next__choice {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
color: var(--wrn-color-text);
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__checkbox-content > .wrn-next__choice {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.wrn-next__choice input {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
accent-color: var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next__choice-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.15rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrn-next__choice-copy small {
|
||||
color: var(--wrn-color-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.wrn-next__radio-group,
|
||||
.wrn-next__checkbox-group {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
|
||||
align-items: stretch;
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
|
||||
min-height: 4.5rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
|
||||
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
|
||||
border-color: var(--wrn-field-color);
|
||||
box-shadow: 0 0 0 1px var(--wrn-field-color);
|
||||
}
|
||||
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
|
||||
padding-block: 0.75rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
|
||||
order: 2;
|
||||
}
|
||||
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
|
||||
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
|
||||
order: 1;
|
||||
}
|
||||
.wrn-next__choice[data-variant="outlined"],
|
||||
.wrn-next__choice[data-variant="floating"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
.wrn-next__choice[data-variant="pill"] {
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
|
||||
}
|
||||
.wrn-next__choice[data-variant="ghost"] {
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
|
||||
}
|
||||
.wrn-next__choice[data-variant="underline"] {
|
||||
padding-block: 0.5rem;
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
.wrn-next--select .wrn-next__field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__color-control,
|
||||
.wrn-next__range-control,
|
||||
.wrn-next__input-group-control,
|
||||
.wrn-next__file-control {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.wrn-next__picker-control,
|
||||
.wrn-next--time-picker .wrn-next__field-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.wrn-next__picker-control > button:first-of-type,
|
||||
.wrn-next__time-trigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.wrn-next__calendar,
|
||||
.wrn-next__time-panel {
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
width: min(100%, 22rem);
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-3);
|
||||
}
|
||||
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
|
||||
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
|
||||
display: none;
|
||||
}
|
||||
.wrn-next__calendar button,
|
||||
.wrn-next__time-panel button {
|
||||
min-height: 2.25rem;
|
||||
border: 0;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.wrn-next__calendar-weekdays,
|
||||
.wrn-next__calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.wrn-next__calendar-weekdays {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.wrn-next__calendar-grid button:hover,
|
||||
.wrn-next__time-panel button:hover {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
.wrn-next__calendar-grid button[aria-pressed="true"],
|
||||
.wrn-next__time-panel button[aria-pressed="true"] {
|
||||
background: var(--wrn-field-color);
|
||||
color: var(--wrn-color-primary-contrast, #fff);
|
||||
}
|
||||
.wrn-next__time-period {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row,
|
||||
.wrn-next__upload-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wrn-next--file-upload-progress .wrn-next__row > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
.wrn-next--field[data-variant="underline"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.wrn-next--field[data-variant="outlined"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-width: 2px;
|
||||
}
|
||||
.wrn-next--field[data-variant="pill"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
.wrn-next--field[data-variant="ghost"]
|
||||
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.wrn-next--field[data-inline="true"],
|
||||
.wrn-next--choice-field[data-inline="true"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
|
||||
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
|
||||
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
|
||||
border-color: var(--wrn-color-danger);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
|
||||
}
|
||||
.wrn-next--strong-password.wrn-next--invalid input,
|
||||
.wrn-next--strong-password input.wrn-invalid {
|
||||
border-color: var(--wrn-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,26 +81,26 @@ component InputNumber {
|
||||
|
||||
shared function componentColor() {
|
||||
if (color === "secondary") {
|
||||
return "var(--wire-color-secondary)"
|
||||
return "var(--wrn-color-secondary)"
|
||||
}
|
||||
|
||||
if (color === "success") {
|
||||
return "var(--wire-color-success)"
|
||||
return "var(--wrn-color-success)"
|
||||
}
|
||||
|
||||
if (color === "warning") {
|
||||
return "var(--wire-color-warning)"
|
||||
return "var(--wrn-color-warning)"
|
||||
}
|
||||
|
||||
if (color === "danger") {
|
||||
return "var(--wire-color-danger)"
|
||||
return "var(--wrn-color-danger)"
|
||||
}
|
||||
|
||||
if (color === "info") {
|
||||
return "var(--wire-color-info)"
|
||||
return "var(--wrn-color-info)"
|
||||
}
|
||||
|
||||
return "var(--wire-color-primary)"
|
||||
return "var(--wrn-color-primary)"
|
||||
}
|
||||
|
||||
shared function hasMin() {
|
||||
@@ -232,26 +232,17 @@ component InputNumber {
|
||||
)
|
||||
}
|
||||
|
||||
// Outputs are resolved by name, so each one is written out. Raw
|
||||
// CustomEvents dispatched on the root -- what this did before -- never
|
||||
// reach a parent @binding.
|
||||
client function dispatchInputNumberEvent(
|
||||
sourceEvent,
|
||||
eventName,
|
||||
action,
|
||||
previousValue,
|
||||
root,
|
||||
customEvent
|
||||
payload
|
||||
) {
|
||||
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, {
|
||||
payload = {
|
||||
component: "InputNumber",
|
||||
name: name,
|
||||
value: currentValue,
|
||||
@@ -261,8 +252,17 @@ component InputNumber {
|
||||
max: hasMax() ? Number(max) : null,
|
||||
step: normalizedStep(),
|
||||
valid: !isInvalid()
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
}
|
||||
|
||||
if (eventName === "input") {
|
||||
output.input(payload)
|
||||
} else if (eventName === "change") {
|
||||
output.change(payload)
|
||||
} else if (eventName === "increment") {
|
||||
output.increment(payload)
|
||||
} else if (eventName === "decrement") {
|
||||
output.decrement(payload)
|
||||
}
|
||||
}
|
||||
|
||||
client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
|
||||
@@ -418,19 +418,20 @@ component InputNumber {
|
||||
data-value='{currentValue}'
|
||||
data-invalid='{isInvalid() ? "true" : "false"}'
|
||||
data-disabled='{disabled ? "true" : "false"}'
|
||||
data-full-width='{fullWidth ? "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}'
|
||||
class='wrn-input-number {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)]'
|
||||
class='wrn-input-number__label'
|
||||
>
|
||||
<span>{label}</span>
|
||||
{#if required}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='text-[var(--wire-color-danger)]'
|
||||
class='wrn-input-number__required'
|
||||
>*</span>
|
||||
{/if}
|
||||
</label>
|
||||
@@ -439,14 +440,14 @@ component InputNumber {
|
||||
{#if description !== "" && variant !== "labeled" && variant !== "seat"}
|
||||
<p
|
||||
id='{descriptionId()}'
|
||||
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
|
||||
class='wrn-input-number__description'
|
||||
>
|
||||
{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)]" : ""}'
|
||||
class='wrn-input-number__control'
|
||||
>
|
||||
{#if variant === "horizontal" && showButtons}
|
||||
<button
|
||||
@@ -455,30 +456,30 @@ component InputNumber {
|
||||
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"}'
|
||||
class='wrn-input-number__button wrn-input-number__button--start'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--minus] size-4'
|
||||
class='icon-[lucide--minus] wrn-input-number__icon'
|
||||
></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"}'
|
||||
class='wrn-input-number__content'
|
||||
>
|
||||
{#if variant === "labeled" || variant === "seat"}
|
||||
<div class='min-w-0 flex-1'>
|
||||
<div class='wrn-input-number__copy'>
|
||||
{#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"}'
|
||||
class='wrn-input-number__inner-label'
|
||||
>
|
||||
{label}
|
||||
{#if required}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='ml-0.5 text-[var(--wire-color-danger)]'
|
||||
class='wrn-input-number__required'
|
||||
>*</span>
|
||||
{/if}
|
||||
</label>
|
||||
@@ -487,7 +488,7 @@ component InputNumber {
|
||||
{#if description !== ""}
|
||||
<span
|
||||
id='{descriptionId()}'
|
||||
class='block truncate text-xs leading-4 text-[var(--wire-color-muted)]'
|
||||
class='wrn-input-number__inner-description'
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
@@ -496,12 +497,12 @@ component InputNumber {
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class='flex min-w-0 items-center {variant === "seat" ? "w-auto shrink-0" : "w-full"}'
|
||||
class='wrn-input-number__field'
|
||||
>
|
||||
{#if prefix !== ""}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='shrink-0 pr-1.5 text-[var(--wire-color-muted)]'
|
||||
class='wrn-input-number__prefix'
|
||||
>
|
||||
{prefix}
|
||||
</span>
|
||||
@@ -531,13 +532,13 @@ component InputNumber {
|
||||
@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"}'
|
||||
class='wrn-input-number__input'
|
||||
/>
|
||||
|
||||
{#if suffix !== ""}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='shrink-0 pl-1.5 text-[var(--wire-color-muted)]'
|
||||
class='wrn-input-number__suffix'
|
||||
>
|
||||
{suffix}
|
||||
</span>
|
||||
@@ -552,11 +553,11 @@ component InputNumber {
|
||||
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"}'
|
||||
class='wrn-input-number__button wrn-input-number__button--end'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--plus] size-4'
|
||||
class='icon-[lucide--plus] wrn-input-number__icon'
|
||||
></span>
|
||||
</button>
|
||||
{:else}
|
||||
@@ -564,7 +565,7 @@ component InputNumber {
|
||||
<div
|
||||
role="group"
|
||||
aria-label='{controlsLabel}'
|
||||
class='flex shrink-0 border-l border-[var(--wire-color-border)] {variant === "vertical" ? "flex-col" : "flex-row"}'
|
||||
class='wrn-input-number__buttons'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -572,11 +573,11 @@ component InputNumber {
|
||||
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"}'
|
||||
class='wrn-input-number__button wrn-input-number__button--decrement'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--minus] size-4'
|
||||
class='icon-[lucide--minus] wrn-input-number__icon'
|
||||
></span>
|
||||
</button>
|
||||
|
||||
@@ -586,11 +587,11 @@ component InputNumber {
|
||||
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"}'
|
||||
class='wrn-input-number__button wrn-input-number__button--increment'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--plus] size-4'
|
||||
class='icon-[lucide--plus] wrn-input-number__icon'
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -603,11 +604,11 @@ component InputNumber {
|
||||
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)]'
|
||||
class='wrn-input-number__message wrn-input-number__message--error'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--circle-alert] size-3.5 shrink-0'
|
||||
class='icon-[lucide--circle-alert] wrn-input-number__error-icon'
|
||||
></span>
|
||||
<span>{validationMessage()}</span>
|
||||
</p>
|
||||
@@ -616,11 +617,66 @@ component InputNumber {
|
||||
{#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
|
||||
<p
|
||||
id='{messageId()}'
|
||||
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
|
||||
class='wrn-input-number__message'
|
||||
>
|
||||
{helpText}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-input-number { position: relative; display: flex; width: 100%; max-width: 24rem; flex-direction: column; gap: 0.375rem; color: var(--wrn-color-text); }
|
||||
.wrn-input-number[data-full-width="true"] { max-width: none; }
|
||||
.wrn-input-number[data-variant="compact"] { width: fit-content; max-width: 100%; }
|
||||
.wrn-input-number[data-disabled="true"] { opacity: 0.6; }
|
||||
.wrn-input-number__label { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--wrn-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1.25rem; }
|
||||
.wrn-input-number__required { margin-left: 0.125rem; color: var(--wrn-color-danger); }
|
||||
.wrn-input-number__description, .wrn-input-number__message { margin: 0; color: var(--wrn-color-muted); font-size: 0.75rem; line-height: 1.25rem; }
|
||||
.wrn-input-number__control { display: flex; min-width: 0; min-height: 2.5rem; overflow: hidden; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-sm); color: var(--wrn-color-text); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); transition: border-color var(--wrn-motion-base), box-shadow var(--wrn-motion-base), background var(--wrn-motion-base); }
|
||||
.wrn-input-number__control:focus-within { border-color: var(--input-number-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--input-number-accent) 18%, transparent); }
|
||||
.wrn-input-number[data-invalid="true"] .wrn-input-number__control { border-color: var(--wrn-color-danger); }
|
||||
.wrn-input-number[data-invalid="true"] .wrn-input-number__control:focus-within { box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 18%, transparent); }
|
||||
.wrn-input-number[data-disabled="true"] .wrn-input-number__control { cursor: not-allowed; background: var(--wrn-color-surface-2); }
|
||||
.wrn-input-number[data-variant="compact"] .wrn-input-number__control { border-radius: 999px; }
|
||||
.wrn-input-number[data-size="xs"] .wrn-input-number__control { min-height: 2rem; font-size: 0.75rem; }
|
||||
.wrn-input-number[data-size="sm"] .wrn-input-number__control { min-height: 2.25rem; font-size: 0.875rem; }
|
||||
.wrn-input-number[data-size="lg"] .wrn-input-number__control { min-height: 3rem; font-size: 1rem; }
|
||||
.wrn-input-number[data-size="xl"] .wrn-input-number__control { min-height: 3.5rem; font-size: 1.125rem; }
|
||||
.wrn-input-number__content { display: flex; min-width: 0; flex: 1; align-items: center; padding-inline: 0.75rem; }
|
||||
.wrn-input-number[data-size="xs"] .wrn-input-number__content { padding-inline: 0.5rem; }
|
||||
.wrn-input-number[data-size="sm"] .wrn-input-number__content { padding-inline: 0.625rem; }
|
||||
.wrn-input-number[data-size="lg"] .wrn-input-number__content { padding-inline: 1rem; }
|
||||
.wrn-input-number[data-size="xl"] .wrn-input-number__content { padding-inline: 1.25rem; }
|
||||
.wrn-input-number[data-variant="labeled"] .wrn-input-number__content { flex-direction: column; align-items: stretch; justify-content: center; gap: 0.125rem; }
|
||||
.wrn-input-number[data-variant="seat"] .wrn-input-number__content { justify-content: space-between; gap: 0.75rem; }
|
||||
.wrn-input-number__copy { min-width: 0; flex: 1; }
|
||||
.wrn-input-number__inner-label, .wrn-input-number__inner-description { display: block; overflow: hidden; color: var(--wrn-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1rem; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wrn-input-number__inner-description { color: var(--wrn-color-muted); font-size: 0.75rem; font-weight: 400; }
|
||||
.wrn-input-number[data-variant="labeled"] .wrn-input-number__inner-label { color: var(--wrn-color-muted); font-size: 0.75rem; font-weight: 500; }
|
||||
.wrn-input-number__field { display: flex; width: 100%; min-width: 0; align-items: center; }
|
||||
.wrn-input-number[data-variant="seat"] .wrn-input-number__field { width: auto; flex: none; }
|
||||
.wrn-input-number__prefix, .wrn-input-number__suffix { flex: none; color: var(--wrn-color-muted); }
|
||||
.wrn-input-number__prefix { padding-right: 0.375rem; } .wrn-input-number__suffix { padding-left: 0.375rem; }
|
||||
.wrn-input-number__input { width: 100%; min-width: 0; flex: 1; padding: 0; border: 0; outline: none; color: var(--wrn-color-text); background: transparent; font: inherit; font-weight: 500; line-height: 1; appearance: textfield; }
|
||||
.wrn-input-number__input::-webkit-inner-spin-button, .wrn-input-number__input::-webkit-outer-spin-button { appearance: none; }
|
||||
.wrn-input-number__input::placeholder { color: var(--wrn-color-muted); }
|
||||
.wrn-input-number__input:read-only { cursor: default; }
|
||||
.wrn-input-number__input:disabled { cursor: not-allowed; }
|
||||
.wrn-input-number:is([data-variant="horizontal"], [data-variant="compact"], [data-variant="seat"]) .wrn-input-number__input { text-align: center; }
|
||||
.wrn-input-number[data-variant="seat"] .wrn-input-number__input { width: 2.5rem; flex: none; }
|
||||
.wrn-input-number__buttons { display: flex; flex: none; border-left: 1px solid var(--wrn-color-border); }
|
||||
.wrn-input-number[data-variant="vertical"] .wrn-input-number__buttons { flex-direction: column; }
|
||||
.wrn-input-number__button { display: inline-flex; width: 2.5rem; flex: none; align-items: center; justify-content: center; padding: 0; border: 0; color: var(--wrn-color-muted); background: transparent; transition: color var(--wrn-motion-fast), background var(--wrn-motion-fast); }
|
||||
.wrn-input-number[data-size="xs"] .wrn-input-number__button { width: 2rem; } .wrn-input-number[data-size="sm"] .wrn-input-number__button { width: 2.25rem; } .wrn-input-number[data-size="lg"] .wrn-input-number__button { width: 3rem; } .wrn-input-number[data-size="xl"] .wrn-input-number__button { width: 3.5rem; }
|
||||
.wrn-input-number__button--start { border-right: 1px solid var(--wrn-color-border); } .wrn-input-number__button--end { border-left: 1px solid var(--wrn-color-border); }
|
||||
.wrn-input-number__button--decrement { border-right: 1px solid var(--wrn-color-border); }
|
||||
.wrn-input-number[data-variant="vertical"] .wrn-input-number__button--decrement { border-right: 0; border-bottom: 1px solid var(--wrn-color-border); }
|
||||
.wrn-input-number__button:hover { color: var(--input-number-accent); background: var(--wrn-color-surface-2); }
|
||||
.wrn-input-number__button:focus-visible { z-index: 1; outline: 2px solid var(--input-number-accent); outline-offset: -2px; }
|
||||
.wrn-input-number__button:disabled { cursor: not-allowed; opacity: 0.4; }
|
||||
.wrn-input-number__icon { width: 1rem; height: 1rem; }
|
||||
.wrn-input-number__message--error { display: flex; align-items: center; gap: 0.375rem; color: var(--wrn-color-danger); }
|
||||
.wrn-input-number__error-icon { width: 0.875rem; height: 0.875rem; flex: none; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,55 @@
|
||||
// Kbd -- a keyboard key.
|
||||
//
|
||||
// <Kbd label="Ctrl" />
|
||||
//
|
||||
// Deliberately small. A kbd element is a kbd element; the work here is making
|
||||
// it look like a key and follow the theme.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
component Kbd {
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "⌘ K"
|
||||
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> }
|
||||
|
||||
view {
|
||||
<kbd {...attrs} class='wrn-kbd {class}' data-size='{size}' data-color='{color}'>
|
||||
<slot>{label}</slot>
|
||||
</kbd>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.6rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
/* The lower edge is what reads as a physical key. */
|
||||
border-bottom-width: 2px;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface-soft);
|
||||
color: var(--wrn-color-text);
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wrn-kbd[data-size="sm"] {
|
||||
min-width: 1.4rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.wrn-kbd[data-size="lg"] {
|
||||
min-width: 2rem;
|
||||
padding: 0.3rem 0.55rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,225 @@
|
||||
// LayoutSplitter -- two panes with a divider the reader can move.
|
||||
//
|
||||
// <LayoutSplitter size={40} minSize={20}>
|
||||
// <div data-slot="start">...</div>
|
||||
// <div data-slot="end">...</div>
|
||||
// </LayoutSplitter>
|
||||
//
|
||||
// The dragging lives in the reactive runtime behind data-wrn-splitter. Pointer
|
||||
// moves fire far too often to route through a client function, and a state
|
||||
// write made inside a pointermove callback is dropped, so the resolved size is
|
||||
// held in the DOM as the --wrn-split custom property and these styles read it.
|
||||
//
|
||||
// This component previously declared resizeStart, resize and resizeEnd with no
|
||||
// pointer handling whatsoever: a caller connected up @resize and received nothing,
|
||||
// for ever, with no error.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
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[])
|
||||
// Named sizeChange rather than resize so it cannot be confused with the
|
||||
// native window event a caller may already be listening for. An earlier
|
||||
// comment here claimed a natively-named output could never reach a parent
|
||||
// binding; that was wrong, and the rename was never what fixed anything.
|
||||
sizeChange(payload: { size: number })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
columns: number = 2
|
||||
gap: string = "md"
|
||||
maxWidth: string = "xl"
|
||||
size: number = 50
|
||||
orientation: string = "horizontal"
|
||||
// Smallest share either pane may take, as a percentage. It also fixes the
|
||||
// upper bound at 100 - minSize, so neither pane can be dragged away to
|
||||
// nothing and left unrecoverable by pointer.
|
||||
minSize: number = 15
|
||||
step: number = 5
|
||||
label: string = "Resize panels"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
// The runtime resolves the size and announces it; this turns that into the
|
||||
// declared output so a parent @resize binding receives it.
|
||||
client function reportResize(sourceEvent) {
|
||||
var detail = sourceEvent ? sourceEvent.detail : null
|
||||
if (!detail) {
|
||||
return
|
||||
}
|
||||
output.sizeChange({ size: detail.size })
|
||||
}
|
||||
|
||||
shared function isVertical() {
|
||||
return orientation === "vertical"
|
||||
}
|
||||
|
||||
shared function lowerBound() {
|
||||
var value = Number(minSize)
|
||||
if (!value || value < 0) {
|
||||
return 15
|
||||
}
|
||||
return Math.min(45, value)
|
||||
}
|
||||
|
||||
shared function upperBound() {
|
||||
return 100 - lowerBound()
|
||||
}
|
||||
|
||||
// Sizes arrive as HTML attributes, so a value outside the bounds is
|
||||
// routine rather than exceptional. Clamp instead of rendering something
|
||||
// the reader cannot undo.
|
||||
shared function currentSize() {
|
||||
var value = Number(size)
|
||||
if (!value || value < 0) {
|
||||
return 50
|
||||
}
|
||||
return Math.min(upperBound(), Math.max(lowerBound(), value))
|
||||
}
|
||||
|
||||
shared function stepSize() {
|
||||
var value = Number(step)
|
||||
return value && value > 0 ? value : 5
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="LayoutSplitter"
|
||||
class='wrn-splitter {class}'
|
||||
data-color='{color}'
|
||||
data-orientation='{orientation}'
|
||||
data-wrn-splitter='{isVertical() ? "vertical" : "horizontal"}'
|
||||
data-wrn-splitter-min='{lowerBound()}'
|
||||
data-wrn-splitter-step='{stepSize()}'
|
||||
style='--wrn-split:{currentSize()}%;'
|
||||
@wrnexus:splitter:resize='reportResize(event)'
|
||||
>
|
||||
<div class="wrn-splitter__pane wrn-splitter__pane--start">
|
||||
<slot name="start"></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wrn-splitter__handle"
|
||||
data-wrn-splitter-handle="true"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
aria-label='{label}'
|
||||
aria-orientation='{isVertical() ? "horizontal" : "vertical"}'
|
||||
aria-valuenow='{currentSize()}'
|
||||
aria-valuemin='{lowerBound()}'
|
||||
aria-valuemax='{upperBound()}'
|
||||
>
|
||||
<span class="wrn-splitter__grip" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
<div class="wrn-splitter__pane wrn-splitter__pane--end">
|
||||
<slot name="end"></slot>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-splitter {
|
||||
--splitter-accent: var(--wrn-color-primary);
|
||||
display: grid;
|
||||
/* The first track follows the size the runtime resolves. */
|
||||
grid-template-columns: var(--wrn-split, 50%) auto minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wrn-splitter[data-color="secondary"] {
|
||||
--splitter-accent: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-splitter[data-color="success"] {
|
||||
--splitter-accent: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-splitter[data-color="danger"] {
|
||||
--splitter-accent: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-splitter[data-color="info"] {
|
||||
--splitter-accent: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-splitter[data-orientation="vertical"] {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: var(--wrn-split, 50%) auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.wrn-splitter__pane {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.wrn-splitter__handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 0.25rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
/* Without this the pointer drag selects the text in both panes. */
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wrn-splitter[data-orientation="vertical"] .wrn-splitter__handle {
|
||||
padding: 0.25rem 0;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.wrn-splitter__grip {
|
||||
display: block;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
min-height: 1.5rem;
|
||||
border-radius: 999px;
|
||||
background: var(--wrn-color-border);
|
||||
transition: background 140ms ease;
|
||||
}
|
||||
|
||||
.wrn-splitter[data-orientation="vertical"] .wrn-splitter__grip {
|
||||
width: 100%;
|
||||
min-width: 1.5rem;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.wrn-splitter__handle:hover .wrn-splitter__grip,
|
||||
.wrn-splitter[data-wrn-splitter-dragging="true"] .wrn-splitter__grip {
|
||||
background: var(--splitter-accent);
|
||||
}
|
||||
|
||||
.wrn-splitter__handle:focus-visible {
|
||||
outline: 2px solid var(--splitter-accent);
|
||||
outline-offset: -2px;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
}
|
||||
|
||||
/*
|
||||
* Two panes side by side stop making sense on a phone. Stacking them keeps
|
||||
* both readable, and the divider stops being draggable because the grid
|
||||
* no longer has a second track to trade against.
|
||||
*/
|
||||
@media (max-width: 639px) {
|
||||
.wrn-splitter,
|
||||
.wrn-splitter[data-orientation="vertical"] {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
|
||||
.wrn-splitter__handle {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,95 @@ component LegendIndicator {
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--legend-indicator wire-next--variant-{variant} {class}">
|
||||
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--legend-indicator wrn-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}
|
||||
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--legend-indicator {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius);
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--legend-indicator > p { margin: 0; color: var(--wrn-color-muted); }
|
||||
.wrn-next--legend-indicator > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.wrn-next--legend-indicator > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+114
-24
@@ -1,3 +1,13 @@
|
||||
// Link -- a text link that follows the theme.
|
||||
//
|
||||
// <Link href="/docs" label="Documentation" />
|
||||
// <Link href="https://example.com" label="Spec" external={true} />
|
||||
//
|
||||
// external adds rel="noopener noreferrer" and an outbound icon, so the reader
|
||||
// is told the link leaves the site before they follow it.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
component Link {
|
||||
props {
|
||||
size: string = "default"
|
||||
@@ -6,42 +16,122 @@ component Link {
|
||||
href: string = "#"
|
||||
target: string = ""
|
||||
rel: string = ""
|
||||
underline: string = "hover"
|
||||
external: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<a
|
||||
{...attrs}
|
||||
data-ui-component="Link"
|
||||
class='wrn-link {class}'
|
||||
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'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-underline='{underline}'
|
||||
data-external='{external}'
|
||||
>
|
||||
<span class="truncate">{label}</span>
|
||||
|
||||
{#if external}
|
||||
<span
|
||||
class="icon-[lucide--external-link] size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{/if}
|
||||
|
||||
<span class="wrn-link__label">{label}</span>
|
||||
<span
|
||||
class="icon-[lucide--external-link] wrn-link__icon"
|
||||
data-show="external"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<slot></slot>
|
||||
</a>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-link {
|
||||
--link-tone: var(--wrn-color-primary);
|
||||
--link-tone-hover: var(--wrn-color-primary-hover);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
color: var(--link-tone);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-underline-offset: 4px;
|
||||
transition: color 140ms ease;
|
||||
}
|
||||
|
||||
.wrn-link[data-color="secondary"] {
|
||||
--link-tone: var(--wrn-color-secondary);
|
||||
--link-tone-hover: var(--wrn-color-secondary-hover);
|
||||
}
|
||||
|
||||
.wrn-link[data-color="success"] {
|
||||
--link-tone: var(--wrn-color-success);
|
||||
--link-tone-hover: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-link[data-color="warning"] {
|
||||
--link-tone: var(--wrn-color-warning-text);
|
||||
--link-tone-hover: var(--wrn-color-warning-text);
|
||||
}
|
||||
|
||||
.wrn-link[data-color="danger"] {
|
||||
--link-tone: var(--wrn-color-danger);
|
||||
--link-tone-hover: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-link[data-color="info"] {
|
||||
--link-tone: var(--wrn-color-info);
|
||||
--link-tone-hover: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-link[data-color="neutral"] {
|
||||
--link-tone: var(--wrn-color-text);
|
||||
--link-tone-hover: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wrn-link[data-size="xs"] {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.wrn-link[data-size="md"] {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wrn-link[data-size="lg"] {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.wrn-link[data-underline="always"] {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.wrn-link[data-underline="hover"]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.wrn-link:hover {
|
||||
color: var(--link-tone-hover);
|
||||
}
|
||||
|
||||
.wrn-link:focus-visible {
|
||||
outline: 2px solid var(--wrn-color-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* The label truncates; the outbound icon must not shrink with it. */
|
||||
.wrn-link__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wrn-link__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
component List {
|
||||
outputs {
|
||||
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
@@ -13,89 +17,76 @@ component List {
|
||||
<section
|
||||
data-ui-component="List"
|
||||
aria-label='{title}'
|
||||
class='w-full {class}'
|
||||
class='wrn-list {class}'
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="mb-4">
|
||||
<header class="wrn-list__header">
|
||||
{#if title}
|
||||
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
|
||||
<h3 class="wrn-list__title">{title}</h3>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
|
||||
<p class="wrn-list__description">{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"'
|
||||
class="wrn-list__items"
|
||||
data-variant='{variant}'
|
||||
>
|
||||
{#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"'
|
||||
class="wrn-list__item"
|
||||
data-variant='{variant}'
|
||||
@click='if (!item.href) { output.select({ item: item, index: index }) }'
|
||||
>
|
||||
{#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 } }))'
|
||||
class="wrn-list__row"
|
||||
data-size='{size}'
|
||||
@click='output.select({ 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 class="wrn-list__leading-icon"><span class='{item.icon}' 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" />
|
||||
<img src='{item.imageSrc}' alt='{item.imageAlt || ""}' class="wrn-list__image" 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>
|
||||
<div class="wrn-list__body">
|
||||
<div class="wrn-list__heading">
|
||||
<p class="wrn-list__item-title">{item.title || item.label}</p>
|
||||
{#if item.meta}
|
||||
<span class="shrink-0 text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
|
||||
<span class="wrn-list__meta">{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>
|
||||
<p class="wrn-list__item-description">{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>
|
||||
<span class="wrn-list__badge">{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>
|
||||
<span class="icon-[lucide--chevron-right] wrn-list__chevron" aria-hidden="true"></span>
|
||||
</a>
|
||||
{:else}
|
||||
<div class="flex min-w-0 items-start gap-3 px-4 py-3">
|
||||
<div class="wrn-list__row" data-size='{size}'>
|
||||
{#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 class="wrn-list__leading-icon"><span class='{item.icon}' 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>
|
||||
<div class="wrn-list__body">
|
||||
<p class="wrn-list__item-title">{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>
|
||||
<p class="wrn-list__item-description">{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)]">
|
||||
<li class="wrn-list__empty">
|
||||
No items available.
|
||||
</li>
|
||||
{/each}
|
||||
@@ -104,4 +95,34 @@ component List {
|
||||
<slot></slot>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-list { width: 100%; }
|
||||
.wrn-list__header { margin-bottom: 1rem; }
|
||||
.wrn-list__title { color: var(--wrn-color-text); font-size: 1.125rem; font-weight: 700; }
|
||||
.wrn-list__description { margin-top: 0.25rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
|
||||
.wrn-list__items { overflow: hidden; }
|
||||
.wrn-list__items:is([data-variant="default"], [data-variant="divided"]) > .wrn-list__item + .wrn-list__item { border-top: 1px solid var(--wrn-color-border); }
|
||||
.wrn-list__items[data-variant="card"] { border: 1px solid var(--wrn-color-border); border-radius: calc(var(--wrn-radius) * 1.5); background: var(--wrn-color-surface-raised); }
|
||||
.wrn-list__items[data-variant="separated"] { display: grid; gap: 0.75rem; }
|
||||
.wrn-list__item { min-width: 0; }
|
||||
.wrn-list__item[data-variant="separated"] { border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); background: var(--wrn-color-surface-raised); }
|
||||
.wrn-list__row { display: flex; min-width: 0; align-items: flex-start; gap: 0.75rem; padding: 0.75rem 1rem; outline: none; transition: background var(--wrn-motion-base); }
|
||||
a.wrn-list__row:hover { background: var(--wrn-color-surface-soft); }
|
||||
.wrn-list__row:focus-visible { box-shadow: inset 0 0 0 2px var(--wrn-color-focus); }
|
||||
.wrn-list__row[data-size="sm"] { padding: 0.625rem 0.75rem; }
|
||||
.wrn-list__row[data-size="lg"] { padding: 1rem 1.25rem; }
|
||||
.wrn-list__leading-icon { display: flex; width: 2.5rem; height: 2.5rem; flex: none; align-items: center; justify-content: center; border-radius: var(--wrn-radius); color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); }
|
||||
.wrn-list__leading-icon > span { width: 1.25rem; height: 1.25rem; }
|
||||
.wrn-list__image { width: 3rem; height: 3rem; flex: none; border-radius: var(--wrn-radius); object-fit: cover; }
|
||||
.wrn-list__body { min-width: 0; flex: 1; }
|
||||
.wrn-list__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
|
||||
.wrn-list__item-title { overflow: hidden; color: var(--wrn-color-text); font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wrn-list__meta { flex: none; color: var(--wrn-color-text-muted); font-size: 0.75rem; }
|
||||
.wrn-list__item-description { display: -webkit-box; margin-top: 0.25rem; overflow: hidden; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.25rem; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.wrn-list__badge { display: inline-flex; margin-top: 0.5rem; padding: 0.25rem 0.625rem; border-radius: 999px; color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); font-size: 0.75rem; font-weight: 600; }
|
||||
.wrn-list__chevron { width: 1rem; height: 1rem; flex: none; margin-top: 0.25rem; color: var(--wrn-color-text-muted); transition: color var(--wrn-motion-base), transform var(--wrn-motion-base); }
|
||||
.wrn-list__row:hover .wrn-list__chevron { color: var(--wrn-color-primary); transform: translateX(0.125rem); }
|
||||
.wrn-list__empty { padding: 1.5rem; border: 1px dashed var(--wrn-color-border); border-radius: var(--wrn-radius); color: var(--wrn-color-text-muted); font-size: 0.875rem; text-align: center; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,95 @@ component ListGroup {
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--list-group wire-next--variant-{variant} {class}">
|
||||
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--list-group wrn-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}
|
||||
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-next--list-group {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius);
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
.wrn-next--list-group > p { margin: 0; color: var(--wrn-color-muted); }
|
||||
.wrn-next--list-group > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.wrn-next--list-group > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
component Map {
|
||||
outputs {
|
||||
// markerClick and select both fire for a marker press; select is the
|
||||
// generic name callers reach for, markerClick the explicit one.
|
||||
markerClick(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
zoom(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
@@ -13,37 +21,34 @@ component Map {
|
||||
<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}'
|
||||
class='wrn-map {class}'
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="flex items-start justify-between gap-4 border-b border-[var(--wire-color-border)] p-5">
|
||||
<header class="wrn-map__header">
|
||||
<div>
|
||||
{#if title}
|
||||
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
|
||||
<h3 class="wrn-map__title">{title}</h3>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
|
||||
<p class="wrn-map__description">{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 class="wrn-map__header-icon"><span class="icon-[lucide--map]" 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"'
|
||||
class="wrn-map__viewport"
|
||||
data-size='{size}'
|
||||
>
|
||||
<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;"
|
||||
class="wrn-map__grid"
|
||||
style="background-image: linear-gradient(var(--wrn-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--wrn-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">
|
||||
<div class="wrn-map__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
@@ -52,34 +57,55 @@ component Map {
|
||||
<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)]"
|
||||
class="wrn-map__marker"
|
||||
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 }))'
|
||||
@click='output.markerClick(item); output.select(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">
|
||||
<div class="wrn-map__controls">
|
||||
<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" } }))'
|
||||
class="wrn-map__control"
|
||||
@click='output.zoom({ direction: "in" })'
|
||||
>
|
||||
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--plus] wrn-map__control-icon" 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" } }))'
|
||||
class="wrn-map__control"
|
||||
@click='output.zoom({ direction: "out" })'
|
||||
>
|
||||
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--minus] wrn-map__control-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-map { overflow: hidden; border: 1px solid var(--wrn-color-border); border-radius: calc(var(--wrn-radius) * 1.5); background: var(--wrn-color-surface-raised); box-shadow: var(--wrn-shadow-1); }
|
||||
.wrn-map__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding: 1.25rem; border-bottom: 1px solid var(--wrn-color-border); }
|
||||
.wrn-map__title { color: var(--wrn-color-text); font-size: 1.125rem; font-weight: 700; }
|
||||
.wrn-map__description { margin-top: 0.25rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
|
||||
.wrn-map__header-icon { display: flex; width: 2.5rem; height: 2.5rem; flex: none; align-items: center; justify-content: center; border-radius: var(--wrn-radius); color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); }
|
||||
.wrn-map__header-icon > span { width: 1.25rem; height: 1.25rem; }
|
||||
.wrn-map__viewport { position: relative; isolation: isolate; height: 20rem; overflow: hidden; background: var(--wrn-color-surface-soft); }
|
||||
.wrn-map__viewport[data-size="sm"] { height: 16rem; }
|
||||
.wrn-map__viewport[data-size="lg"] { height: 28rem; }
|
||||
.wrn-map__grid { position: absolute; inset: 0; opacity: 0.5; pointer-events: none; }
|
||||
.wrn-map__content { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 1.5rem; }
|
||||
.wrn-map__marker { position: absolute; display: inline-flex; width: 2.5rem; height: 2.5rem; align-items: center; justify-content: center; padding: 0; border: 4px solid var(--wrn-color-surface-raised); border-radius: 50%; color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); box-shadow: var(--wrn-shadow-3); transition: transform var(--wrn-motion-base) var(--wrn-ease-standard); }
|
||||
.wrn-map__marker:hover { transform: scale(1.1); }
|
||||
.wrn-map__controls { position: absolute; right: 1rem; bottom: 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.wrn-map__control { display: inline-flex; width: 2.5rem; height: 2.5rem; align-items: center; justify-content: center; padding: 0; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); color: var(--wrn-color-text); background: var(--wrn-color-surface-raised); box-shadow: var(--wrn-shadow-1); transition: background var(--wrn-motion-base); }
|
||||
.wrn-map__control:hover { background: var(--wrn-color-surface-soft); }
|
||||
.wrn-map__marker:focus-visible, .wrn-map__control:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: 2px; }
|
||||
.wrn-map__control-icon { width: 1rem; height: 1rem; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
component Marquee {
|
||||
outputs {
|
||||
// Fired when the reader pauses the scroll, by hover, focus or the button.
|
||||
pause(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
resume(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
@@ -15,46 +21,44 @@ component Marquee {
|
||||
<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}'
|
||||
class='wrn-marquee {class}'
|
||||
>
|
||||
<div class="flex items-stretch">
|
||||
<div class="wrn-marquee__layout">
|
||||
{#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 class="wrn-marquee__title">
|
||||
<span class="icon-[lucide--megaphone] wrn-marquee__icon" aria-hidden="true"></span>
|
||||
<span>{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 }))'
|
||||
class="wrn-marquee__window"
|
||||
@mouseenter='paused = true; output.pause({})'
|
||||
@mouseleave='paused = false; output.resume({})'
|
||||
@focusin='paused = true'
|
||||
@focusout='paused = false'
|
||||
>
|
||||
<div
|
||||
class="wire-marquee-track flex w-max min-w-full items-center"
|
||||
class:wire-marquee-paused='paused'
|
||||
class="wrn-marquee__track"
|
||||
class:wrn-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)]"
|
||||
class="wrn-marquee__item"
|
||||
>
|
||||
<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"'
|
||||
class="wrn-marquee__dot"
|
||||
data-color='{item.color || "primary"}'
|
||||
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>
|
||||
<span class="wrn-marquee__meta">{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>
|
||||
<p class="wrn-marquee__empty">{description || "No announcements available."}</p>
|
||||
{/each}
|
||||
|
||||
{#if items.length > 0}
|
||||
@@ -63,9 +67,9 @@ component Marquee {
|
||||
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)]"
|
||||
class="wrn-marquee__item"
|
||||
>
|
||||
<span class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]" aria-hidden="true"></span>
|
||||
<span class="wrn-marquee__dot" aria-hidden="true"></span>
|
||||
<span>{item.label || item.title}</span>
|
||||
</a>
|
||||
{/each}
|
||||
@@ -76,11 +80,11 @@ component Marquee {
|
||||
<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 }))'
|
||||
class="wrn-marquee__toggle"
|
||||
@click='paused = !paused; paused ? output.pause({}) : output.resume({})'
|
||||
>
|
||||
<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>
|
||||
<span class="icon-[lucide--pause] wrn-marquee__icon" data-show='!paused' aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--play] wrn-marquee__icon" data-show='paused' aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -89,21 +93,40 @@ component Marquee {
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-marquee-track {
|
||||
animation: wire-marquee 32s linear infinite;
|
||||
.wrn-marquee { overflow: hidden; border-block: 1px solid var(--wrn-color-border); background: var(--wrn-color-surface-raised); }
|
||||
.wrn-marquee__layout { display: flex; align-items: stretch; }
|
||||
.wrn-marquee__title { position: relative; z-index: 1; display: flex; flex: none; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; border-right: 1px solid var(--wrn-color-border); color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); font-size: 0.875rem; font-weight: 700; }
|
||||
.wrn-marquee__window { position: relative; min-width: 0; flex: 1; overflow: hidden; }
|
||||
.wrn-marquee__track {
|
||||
display: flex; width: max-content; min-width: 100%; align-items: center;
|
||||
animation: wrn-marquee 32s linear infinite;
|
||||
}
|
||||
.wrn-marquee-paused { animation-play-state: paused; }
|
||||
.wrn-marquee__item { display: flex; flex: none; align-items: center; gap: 0.75rem; padding: 0.75rem 1.25rem; color: var(--wrn-color-text); font-size: 0.875rem; font-weight: 500; transition: color var(--wrn-motion-base), background var(--wrn-motion-base); }
|
||||
.wrn-marquee__item:hover { color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); }
|
||||
.wrn-marquee__item:focus-visible, .wrn-marquee__toggle:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: -2px; }
|
||||
.wrn-marquee__dot { width: 0.5rem; height: 0.5rem; flex: none; border-radius: 50%; background: var(--wrn-color-primary); }
|
||||
.wrn-marquee__dot[data-color="danger"] { background: var(--wrn-color-danger); }
|
||||
.wrn-marquee__dot[data-color="warning"] { background: var(--wrn-color-warning); }
|
||||
.wrn-marquee__dot[data-color="success"] { background: var(--wrn-color-success); }
|
||||
.wrn-marquee__meta { color: var(--wrn-color-text-muted); font-size: 0.75rem; }
|
||||
.wrn-marquee__empty { padding: 0.75rem 1.25rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; }
|
||||
.wrn-marquee__toggle { display: flex; flex: none; align-items: center; justify-content: center; padding-inline: 1rem; border: 0; border-left: 1px solid var(--wrn-color-border); color: var(--wrn-color-text-muted); background: transparent; transition: color var(--wrn-motion-base), background var(--wrn-motion-base); }
|
||||
.wrn-marquee__toggle:hover { color: var(--wrn-color-text); background: var(--wrn-color-surface-soft); }
|
||||
.wrn-marquee__icon { width: 1rem; height: 1rem; }
|
||||
@media (min-width: 40rem) { .wrn-marquee__title { padding-inline: 1.25rem; } }
|
||||
|
||||
.wire-marquee-paused {
|
||||
.wrn-marquee-paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes wire-marquee {
|
||||
@keyframes wrn-marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-marquee-track {
|
||||
.wrn-marquee__track {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,344 @@
|
||||
// MegaMenu -- a trigger and a wide panel of grouped links.
|
||||
//
|
||||
// <MegaMenu label="Products" columns='[{"heading":"Platform",
|
||||
// "items":[{"label":"Runtime","href":"/runtime"}]}]' />
|
||||
//
|
||||
// Deliberately one level deep. A mega menu exists to show breadth flat, so
|
||||
// everything is one click away; nesting inside the panel buries content behind
|
||||
// hover-within-hover and is close to unusable by keyboard and touch. Use Nav
|
||||
// when you actually want cascading submenus.
|
||||
//
|
||||
// The panel carries data-wrn-anchored so the runtime clamp keeps it inside the
|
||||
// viewport instead of hanging off the edge of a wide layout.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
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)
|
||||
open(payload: { sourceEvent: Event })
|
||||
close(payload: { reason: string })
|
||||
select(payload: { item: object; value: string })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Mega Menu"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
size: string = "default"
|
||||
label: string = "Menu"
|
||||
icon: string = ""
|
||||
columns: unknown[] = []
|
||||
footer: string = ""
|
||||
defaultOpen: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
shared function columnList() {
|
||||
return Array.isArray(columns) ? columns : []
|
||||
}
|
||||
|
||||
shared function itemsOf(column) {
|
||||
return column && Array.isArray(column.items) ? column.items : []
|
||||
}
|
||||
|
||||
client function showPanel(sourceEvent) {
|
||||
visible = true
|
||||
output.open({ sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function hidePanel(reason) {
|
||||
visible = false
|
||||
output.close({ reason: reason })
|
||||
}
|
||||
|
||||
client function togglePanel(sourceEvent) {
|
||||
if (visible) {
|
||||
hidePanel("toggle")
|
||||
} else {
|
||||
showPanel(sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (sourceEvent.key === "Escape" && visible) {
|
||||
sourceEvent.preventDefault()
|
||||
hidePanel("escape")
|
||||
}
|
||||
}
|
||||
|
||||
client function choose(item) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
}
|
||||
output.select({ item: item, value: item.value || item.label || "" })
|
||||
hidePanel("select")
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="MegaMenu"
|
||||
class='wrn-mega {class}'
|
||||
data-color='{color}'
|
||||
data-size='{size}'
|
||||
data-open='{visible}'
|
||||
@keydown='handleKeydown(event)'
|
||||
@mouseleave='hidePanel("pointer-leave")'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wrn-mega__trigger"
|
||||
aria-haspopup="true"
|
||||
aria-expanded='{visible}'
|
||||
@click='togglePanel(event)'
|
||||
@mouseenter='showPanel(event)'
|
||||
@focus='showPanel(event)'
|
||||
>
|
||||
<span class='wrn-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
|
||||
<span class="wrn-mega__trigger-label">{label}</span>
|
||||
<span class="wrn-mega__arrow" aria-hidden="true">›</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="wrn-mega__panel"
|
||||
data-wrn-anchored="true"
|
||||
data-show="visible"
|
||||
data-wrn-roving="both"
|
||||
>
|
||||
<div class="wrn-mega__columns">
|
||||
{#each columnList() as column}
|
||||
<section class="wrn-mega__column">
|
||||
<h3 class="wrn-mega__heading" data-show="column.heading">{column.heading}</h3>
|
||||
<ul class="wrn-mega__list">
|
||||
{#each itemsOf(column) as item}
|
||||
<li>
|
||||
<a
|
||||
class="wrn-mega__link"
|
||||
href='{item.href || "#"}'
|
||||
data-wrn-roving-item="true"
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
@click='choose(item)'
|
||||
>
|
||||
<span
|
||||
class='wrn-mega__link-icon {item.icon}'
|
||||
data-show="item.icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="wrn-mega__link-body">
|
||||
<span class="wrn-mega__link-label">{item.label}</span>
|
||||
<span class="wrn-mega__link-description" data-show="item.description">
|
||||
{item.description}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p class="wrn-mega__footer" data-show="footer">{footer}</p>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-mega {
|
||||
--mega-accent: var(--wrn-color-primary);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wrn-mega[data-color="secondary"] {
|
||||
--mega-accent: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-mega[data-color="success"] {
|
||||
--mega-accent: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-mega[data-color="danger"] {
|
||||
--mega-accent: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-mega[data-color="info"] {
|
||||
--mega-accent: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-mega[data-size="sm"] {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wrn-mega[data-size="lg"] {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wrn-mega__trigger {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--wrn-color-text-muted);
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-mega__trigger:hover,
|
||||
.wrn-mega[data-open="true"] .wrn-mega__trigger {
|
||||
background: var(--wrn-color-surface-soft);
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wrn-mega__trigger:focus-visible {
|
||||
outline: 2px solid var(--mega-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-mega__arrow {
|
||||
display: inline-block;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.wrn-mega[data-open="true"] .wrn-mega__arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/*
|
||||
* The panel is offset below the trigger for looks, which used to close the
|
||||
* menu on the way to it: that offset is dead space belonging to neither
|
||||
* element, so crossing it fired mouseleave on the root. The bridge below
|
||||
* covers the gap with a descendant, so the pointer never actually leaves.
|
||||
*/
|
||||
.wrn-mega__panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
height: 0.6rem;
|
||||
}
|
||||
|
||||
.wrn-mega__panel {
|
||||
position: absolute;
|
||||
z-index: 40;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 0;
|
||||
width: max-content;
|
||||
max-width: min(60rem, calc(100vw - 2rem));
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-lg);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
}
|
||||
|
||||
.wrn-mega__columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.wrn-mega__column {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wrn-mega__heading {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wrn-mega__list {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.wrn-mega__link {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.5rem;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
color: var(--wrn-color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wrn-mega__link:hover {
|
||||
background: var(--wrn-color-surface-soft);
|
||||
}
|
||||
|
||||
.wrn-mega__link:focus-visible {
|
||||
outline: 2px solid var(--mega-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-mega__link[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wrn-mega__link-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wrn-mega__link-label {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wrn-mega__link-description {
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wrn-mega__footer {
|
||||
margin: 1rem 0 0;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--wrn-color-border);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* On a phone the panel stops floating and becomes part of the flow. A
|
||||
* pointer-opened overlay pinned to a trigger is unreachable on touch, and
|
||||
* a 60rem grid has nowhere to go on a 360px screen.
|
||||
*/
|
||||
@media (max-width: 767px) {
|
||||
.wrn-mega {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wrn-mega__panel {
|
||||
position: static;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wrn-mega__columns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+132
-132
@@ -59,7 +59,7 @@ component Modal {
|
||||
// )
|
||||
//
|
||||
// The listener is on the modal root, so the event only ever closes the
|
||||
// modal the dispatching element is actually inside -- no ids to wire up
|
||||
// modal the dispatching element is actually inside -- no ids to wrn up
|
||||
// and no way to close somebody else's modal by accident.
|
||||
client function hideModal(reason, sourceEvent) {
|
||||
visible = false
|
||||
@@ -106,13 +106,13 @@ component Modal {
|
||||
data-scrollable='{scrollable ? "true" : "false"}'
|
||||
data-scroll='{scrollBehavior}'
|
||||
data-destructive='{destructive ? "true" : "false"}'
|
||||
class='wire-modal {class}'
|
||||
class='wrn-modal {class}'
|
||||
@wrnexus:modal:close='hideModal("api", event)'
|
||||
>
|
||||
{#if triggerLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__trigger"
|
||||
class="wrn-modal__trigger"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{isOpen() ? "true" : "false"}'
|
||||
@click='showModal(event)'
|
||||
@@ -129,7 +129,7 @@ component Modal {
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class="wire-modal__trigger-slot"
|
||||
class="wrn-modal__trigger-slot"
|
||||
@click='showModal(event)'
|
||||
>
|
||||
<slot
|
||||
@@ -139,42 +139,42 @@ component Modal {
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="wire-modal__layer"
|
||||
class="wrn-modal__layer"
|
||||
data-show="isOpen()"
|
||||
role="presentation"
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__backdrop"
|
||||
class="wrn-modal__backdrop"
|
||||
aria-label='{closeLabel}'
|
||||
@click='if (closeOnBackdrop) { cancelModal(event) }'
|
||||
>
|
||||
</button>
|
||||
|
||||
<section
|
||||
class="wire-modal__panel"
|
||||
class="wrn-modal__panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label='{label || title}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<header
|
||||
class="wire-modal__header"
|
||||
class="wrn-modal__header"
|
||||
>
|
||||
<div
|
||||
class="wire-modal__heading"
|
||||
class="wrn-modal__heading"
|
||||
>
|
||||
{#if icon}
|
||||
<span
|
||||
class='wire-modal__icon {icon}'
|
||||
class='wrn-modal__icon {icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-modal__heading-copy"
|
||||
class="wrn-modal__heading-copy"
|
||||
>
|
||||
<slot
|
||||
name="header"
|
||||
@@ -192,7 +192,7 @@ component Modal {
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__close"
|
||||
class="wrn-modal__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hideModal("close-button", event)'
|
||||
>
|
||||
@@ -215,17 +215,17 @@ component Modal {
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="wire-modal__body"
|
||||
class="wrn-modal__body"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
{#if showFooter}
|
||||
<footer
|
||||
class="wire-modal__footer"
|
||||
class="wrn-modal__footer"
|
||||
>
|
||||
<div
|
||||
class="wire-modal__custom-footer"
|
||||
class="wrn-modal__custom-footer"
|
||||
>
|
||||
<slot
|
||||
name="footer"
|
||||
@@ -234,12 +234,12 @@ component Modal {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wire-modal__actions"
|
||||
class="wrn-modal__actions"
|
||||
>
|
||||
{#if cancelLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__button wire-modal__button--secondary"
|
||||
class="wrn-modal__button wrn-modal__button--secondary"
|
||||
@click='cancelModal(event)'
|
||||
>
|
||||
{#if cancelIcon}
|
||||
@@ -256,13 +256,13 @@ component Modal {
|
||||
{#if confirmLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__button wire-modal__button--primary"
|
||||
class="wrn-modal__button wrn-modal__button--primary"
|
||||
disabled='{confirmDisabled || confirmLoading}'
|
||||
@click='confirmModal(event)'
|
||||
>
|
||||
{#if confirmLoading}
|
||||
<span
|
||||
class="wire-modal__spinner"
|
||||
class="wrn-modal__spinner"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
@@ -285,63 +285,63 @@ component Modal {
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-modal {
|
||||
--modal-accent: var(--wire-color-primary);
|
||||
--modal-soft: var(--wire-color-primary-soft);
|
||||
--modal-contrast: var(--wire-color-primary-contrast);
|
||||
.wrn-modal {
|
||||
--modal-accent: var(--wrn-color-primary);
|
||||
--modal-soft: var(--wrn-color-primary-soft);
|
||||
--modal-contrast: var(--wrn-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);
|
||||
.wrn-modal[data-color="secondary"] {
|
||||
--modal-accent: var(--wrn-color-secondary);
|
||||
--modal-soft: var(--wrn-color-secondary-soft);
|
||||
--modal-contrast: var(--wrn-color-secondary-contrast);
|
||||
}
|
||||
|
||||
/*
|
||||
* Fallbacks below (the second var() argument): the theme token generator
|
||||
* (packages/styles/src/theme.ts) only emits a real -contrast token for
|
||||
* primary and secondary. info, success, and danger have no contrast
|
||||
* token defined at all, so var(--wire-color-info-contrast) with no
|
||||
* token defined at all, so var(--wrn-color-info-contrast) with no
|
||||
* fallback resolves to nothing, and --modal-contrast becomes invalid --
|
||||
* which made confirm-button and solid-panel text unreadable. White is a
|
||||
* safe default against these saturated colors until the theme package
|
||||
* defines real tokens for them.
|
||||
*/
|
||||
.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, white);
|
||||
.wrn-modal[data-color="info"] {
|
||||
--modal-accent: var(--wrn-color-info);
|
||||
--modal-soft: var(--wrn-color-info-soft);
|
||||
--modal-contrast: var(--wrn-color-info-contrast, white);
|
||||
}
|
||||
|
||||
.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, white);
|
||||
.wrn-modal[data-color="success"] {
|
||||
--modal-accent: var(--wrn-color-success);
|
||||
--modal-soft: var(--wrn-color-success-soft);
|
||||
--modal-contrast: var(--wrn-color-success-contrast, white);
|
||||
}
|
||||
|
||||
.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);
|
||||
.wrn-modal[data-color="warning"] {
|
||||
--modal-accent: var(--wrn-color-warning);
|
||||
--modal-soft: var(--wrn-color-warning-soft);
|
||||
--modal-contrast: var(--wrn-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, white);
|
||||
.wrn-modal[data-color="danger"],
|
||||
.wrn-modal[data-destructive="true"] {
|
||||
--modal-accent: var(--wrn-color-danger);
|
||||
--modal-soft: var(--wrn-color-danger-soft);
|
||||
--modal-contrast: var(--wrn-color-on-danger, white);
|
||||
}
|
||||
|
||||
.wire-modal__trigger,
|
||||
.wire-modal__trigger-slot {
|
||||
.wrn-modal__trigger,
|
||||
.wrn-modal__trigger-slot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.wire-modal__trigger {
|
||||
.wrn-modal__trigger {
|
||||
appearance: none;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
@@ -356,22 +356,22 @@ component Modal {
|
||||
transition: opacity 150ms ease, transform 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.wire-modal__trigger:hover {
|
||||
.wrn-modal__trigger:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.wire-modal__trigger:active {
|
||||
.wrn-modal__trigger:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.wire-modal__trigger:focus-visible {
|
||||
.wrn-modal__trigger:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--modal-accent) 40%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
* Hidden by default so the SSR-rendered HTML never paints the layer before
|
||||
* hydration runs. data-open on the wire-modal root is evaluated and
|
||||
* hydration runs. data-open on the wrn-modal root is evaluated and
|
||||
* serialized to a real true or false string at render time -- a plain
|
||||
* bind, not the raw-expression data-show directive -- so this selector
|
||||
* is correct on first paint with zero flash, with no dependency on
|
||||
@@ -382,7 +382,7 @@ component Modal {
|
||||
* animate -- a box that starts at display: none has no prior frame to
|
||||
* transition from.
|
||||
*/
|
||||
.wire-modal__layer {
|
||||
.wrn-modal__layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1250;
|
||||
@@ -395,24 +395,24 @@ component Modal {
|
||||
transition: opacity 180ms ease, visibility 0s linear 180ms;
|
||||
}
|
||||
|
||||
.wire-modal[data-open="true"] .wire-modal__layer {
|
||||
.wrn-modal[data-open="true"] .wrn-modal__layer {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity 180ms ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.wire-modal[data-placement="top"] .wire-modal__layer {
|
||||
.wrn-modal[data-placement="top"] .wrn-modal__layer {
|
||||
align-items: flex-start;
|
||||
padding-top: clamp(1rem, 8vh, 5rem);
|
||||
}
|
||||
|
||||
.wire-modal[data-scroll="page"] .wire-modal__layer {
|
||||
.wrn-modal[data-scroll="page"] .wrn-modal__layer {
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
padding: 2.5rem 1rem;
|
||||
}
|
||||
|
||||
.wire-modal__backdrop {
|
||||
.wrn-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
@@ -423,22 +423,22 @@ component Modal {
|
||||
backdrop-filter: blur(9px);
|
||||
}
|
||||
|
||||
.wire-modal__panel {
|
||||
.wrn-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);
|
||||
color: var(--wrn-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));
|
||||
var(--wrn-color-surface-raised);
|
||||
border: 1px solid color-mix(in srgb, var(--modal-accent) 18%, var(--wrn-color-border));
|
||||
border-radius: 1.35rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
@@ -449,71 +449,71 @@ component Modal {
|
||||
transition: opacity 180ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.wire-modal[data-open="true"] .wire-modal__panel {
|
||||
.wrn-modal[data-open="true"] .wrn-modal__panel {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wire-modal[data-size="xs"] .wire-modal__panel {
|
||||
.wrn-modal[data-size="xs"] .wrn-modal__panel {
|
||||
width: min(19rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="sm"] .wire-modal__panel {
|
||||
.wrn-modal[data-size="sm"] .wrn-modal__panel {
|
||||
width: min(25rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="lg"] .wire-modal__panel {
|
||||
.wrn-modal[data-size="lg"] .wrn-modal__panel {
|
||||
width: min(44rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="xl"] .wire-modal__panel {
|
||||
.wrn-modal[data-size="xl"] .wrn-modal__panel {
|
||||
width: min(64rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="full"] .wire-modal__panel {
|
||||
.wrn-modal[data-size="full"] .wrn-modal__panel {
|
||||
width: calc(100vw - 2rem);
|
||||
height: calc(100vh - 2rem);
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.wire-modal[data-scroll="page"] .wire-modal__panel {
|
||||
.wrn-modal[data-scroll="page"] .wrn-modal__panel {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="soft"] .wire-modal__panel {
|
||||
.wrn-modal[data-variant="soft"] .wrn-modal__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--modal-soft), transparent 68%),
|
||||
var(--wire-color-surface-raised);
|
||||
var(--wrn-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="outline"] .wire-modal__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
.wrn-modal[data-variant="outline"] .wrn-modal__panel {
|
||||
background: var(--wrn-color-surface-raised);
|
||||
box-shadow: 0 24px 72px color-mix(in srgb, black 24%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__panel {
|
||||
.wrn-modal[data-variant="solid"] .wrn-modal__panel {
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal__header {
|
||||
.wrn-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);
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__heading {
|
||||
.wrn-modal__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-modal__icon {
|
||||
.wrn-modal__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
@@ -521,35 +521,35 @@ component Modal {
|
||||
color: var(--modal-accent);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__icon {
|
||||
.wrn-modal[data-variant="solid"] .wrn-modal__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy {
|
||||
.wrn-modal__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy h2,
|
||||
.wire-modal__heading-copy p {
|
||||
.wrn-modal__heading-copy h2,
|
||||
.wrn-modal__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy h2 {
|
||||
.wrn-modal__heading-copy h2 {
|
||||
color: inherit;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy p {
|
||||
color: var(--wire-color-text-muted);
|
||||
.wrn-modal__heading-copy p {
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__heading-copy p {
|
||||
.wrn-modal[data-variant="solid"] .wrn-modal__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
@@ -559,7 +559,7 @@ component Modal {
|
||||
* 2.15rem button with a ~2px content box -- which squeezed the icon to
|
||||
* 0.4px wide and read as "the close button has no icon".
|
||||
*/
|
||||
.wire-modal__close {
|
||||
.wrn-modal__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -568,7 +568,7 @@ component Modal {
|
||||
padding: 0;
|
||||
width: 2.15rem;
|
||||
height: 2.15rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
color: var(--wrn-color-text-muted);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9999px;
|
||||
@@ -582,34 +582,34 @@ component Modal {
|
||||
* missing rather than subtle. Derive it from the panel contrast color
|
||||
* instead, and give it a faint ring so it is unmistakably a control.
|
||||
*/
|
||||
.wire-modal[data-variant="solid"] .wire-modal__close {
|
||||
.wrn-modal[data-variant="solid"] .wrn-modal__close {
|
||||
color: color-mix(in srgb, currentColor 82%, transparent);
|
||||
border-color: color-mix(in srgb, currentColor 35%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__close:hover {
|
||||
.wrn-modal[data-variant="solid"] .wrn-modal__close:hover {
|
||||
color: currentColor;
|
||||
background: color-mix(in srgb, black 18%, transparent);
|
||||
border-color: color-mix(in srgb, currentColor 55%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal__close:hover {
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-soft);
|
||||
.wrn-modal__close:hover {
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface-soft);
|
||||
}
|
||||
|
||||
.wire-modal__close:focus-visible {
|
||||
.wrn-modal__close:focus-visible {
|
||||
color: var(--modal-accent);
|
||||
border-color: color-mix(in srgb, var(--modal-accent) 45%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-modal__close:active {
|
||||
.wrn-modal__close:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
/* Never let the glyph be shrunk by the flex container. */
|
||||
.wire-modal__close svg {
|
||||
.wrn-modal__close svg {
|
||||
flex: 0 0 auto;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@@ -628,46 +628,46 @@ component Modal {
|
||||
* block scanner treats a quote as a string delimiter while it counts
|
||||
* braces, so a stray one breaks parsing of the whole component.
|
||||
*/
|
||||
.wire-modal__body {
|
||||
.wrn-modal__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 1.4rem;
|
||||
color: var(--wire-color-text);
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
/*
|
||||
* :where() keeps this at the specificity of .wire-modal__body alone, so it
|
||||
* :where() keeps this at the specificity of .wrn-modal__body alone, so it
|
||||
* outranks a global element selector but still yields to any class the app
|
||||
* puts on its own slot content (an error message, a muted caption). A
|
||||
* plain .wire-modal__body p list would have quietly overridden those.
|
||||
* plain .wrn-modal__body p list would have quietly overridden those.
|
||||
*/
|
||||
.wire-modal__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
|
||||
.wrn-modal__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__body {
|
||||
.wrn-modal[data-variant="solid"] .wrn-modal__body {
|
||||
color: var(--modal-contrast);
|
||||
}
|
||||
|
||||
.wire-modal[data-scrollable="true"] .wire-modal__body {
|
||||
.wrn-modal[data-scrollable="true"] .wrn-modal__body {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wire-modal__footer {
|
||||
.wrn-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);
|
||||
border-top: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__custom-footer:empty {
|
||||
.wrn-modal__custom-footer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-modal__actions {
|
||||
.wrn-modal__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@@ -675,7 +675,7 @@ component Modal {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.wire-modal__button {
|
||||
.wrn-modal__button {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -691,17 +691,17 @@ component Modal {
|
||||
transition: transform 150ms ease, opacity 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.wire-modal__button:active {
|
||||
.wrn-modal__button:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-modal__button--secondary {
|
||||
color: var(--wire-color-text);
|
||||
.wrn-modal__button--secondary {
|
||||
color: var(--wrn-color-text);
|
||||
background: transparent;
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__button--primary {
|
||||
.wrn-modal__button--primary {
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border: 1px solid transparent;
|
||||
@@ -713,73 +713,73 @@ component Modal {
|
||||
* Darken the fill slightly and add a light border so the button still
|
||||
* reads as a distinct, clickable pill instead of blending into the panel.
|
||||
*/
|
||||
.wire-modal[data-variant="solid"] .wire-modal__button--primary {
|
||||
.wrn-modal[data-variant="solid"] .wrn-modal__button--primary {
|
||||
background: color-mix(in srgb, black 18%, var(--modal-accent));
|
||||
border-color: color-mix(in srgb, white 32%, transparent);
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, white 12%, transparent) inset;
|
||||
}
|
||||
|
||||
.wire-modal__button:disabled {
|
||||
.wrn-modal__button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-modal__spinner {
|
||||
.wrn-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;
|
||||
animation: wrn-modal-spin 750ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wire-modal-spin {
|
||||
@keyframes wrn-modal-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-modal__layer {
|
||||
.wrn-modal__layer {
|
||||
align-items: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wire-modal__panel,
|
||||
.wire-modal[data-size="xs"] .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 {
|
||||
.wrn-modal__panel,
|
||||
.wrn-modal[data-size="xs"] .wrn-modal__panel,
|
||||
.wrn-modal[data-size="sm"] .wrn-modal__panel,
|
||||
.wrn-modal[data-size="lg"] .wrn-modal__panel,
|
||||
.wrn-modal[data-size="xl"] .wrn-modal__panel {
|
||||
width: 100%;
|
||||
max-height: 92vh;
|
||||
border-radius: 1.25rem 1.25rem 0 0;
|
||||
}
|
||||
|
||||
.wire-modal__footer {
|
||||
.wrn-modal__footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wire-modal__actions {
|
||||
.wrn-modal__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wire-modal__button {
|
||||
.wrn-modal__button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-modal__button,
|
||||
.wire-modal__spinner,
|
||||
.wire-modal__layer,
|
||||
.wire-modal__panel,
|
||||
.wire-modal__close {
|
||||
.wrn-modal__button,
|
||||
.wrn-modal__spinner,
|
||||
.wrn-modal__layer,
|
||||
.wrn-modal__panel,
|
||||
.wrn-modal__close {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wire-modal__panel {
|
||||
.wrn-modal__panel {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,354 @@
|
||||
// Nav -- a navigation link list, flat or with submenus.
|
||||
//
|
||||
// <Nav items='[{"label":"Home","href":"/","value":"home"}]' active="home" />
|
||||
//
|
||||
// An item carrying its own items array becomes a submenu. Depth is capped at
|
||||
// three levels: this template language has no component recursion, so each
|
||||
// level is written out, and three covers any realistic navigation.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
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)
|
||||
select(payload: { item: object; value: string })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Nav"
|
||||
size: string = "default"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
label: string = "Main"
|
||||
collapsible: boolean = true
|
||||
toggleLabel: string = "Menu"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state expanded = false
|
||||
|
||||
functions {
|
||||
shared function itemList() {
|
||||
return Array.isArray(items) ? items : []
|
||||
}
|
||||
|
||||
shared function childrenOf(item) {
|
||||
return item && Array.isArray(item.items) ? item.items : []
|
||||
}
|
||||
|
||||
shared function isActive(item) {
|
||||
return Boolean(item.value) && item.value === active
|
||||
}
|
||||
|
||||
shared function rovingAxis() {
|
||||
return orientation === "vertical" ? "vertical" : "horizontal"
|
||||
}
|
||||
|
||||
client function choose(item) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
}
|
||||
output.select({ item: item, value: item.value || "" })
|
||||
}
|
||||
|
||||
client function toggleMenu() {
|
||||
expanded = !expanded
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
<nav
|
||||
{...attrs}
|
||||
data-ui-component="Nav"
|
||||
class='wrn-nav {class}'
|
||||
data-orientation='{orientation}'
|
||||
data-color='{color}'
|
||||
data-size='{size}'
|
||||
data-expanded='{expanded}'
|
||||
role="navigation"
|
||||
aria-label='{label}'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wrn-nav__toggle"
|
||||
data-show="collapsible"
|
||||
aria-expanded='{expanded}'
|
||||
aria-label='{toggleLabel}'
|
||||
@click='toggleMenu()'
|
||||
>
|
||||
<span class="wrn-nav__toggle-bar" aria-hidden="true"></span>
|
||||
<span class="wrn-nav__toggle-text">{toggleLabel}</span>
|
||||
</button>
|
||||
|
||||
<ul class="wrn-nav__list" data-wrn-roving='{rovingAxis()}'>
|
||||
{#each itemList() as item}
|
||||
<li class="wrn-nav__item" data-has-children='{childrenOf(item).length > 0}'>
|
||||
<a
|
||||
class="wrn-nav__link"
|
||||
href='{item.href || "#"}'
|
||||
data-wrn-roving-item="true"
|
||||
data-active='{isActive(item)}'
|
||||
aria-current='{isActive(item) ? "page" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
@click='choose(item)'
|
||||
>
|
||||
<span
|
||||
class='wrn-nav__icon {item.icon}'
|
||||
data-show="item.icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="wrn-nav__label">{item.label}</span>
|
||||
<span class="wrn-nav__badge" data-show="item.badge">{item.badge}</span>
|
||||
<span
|
||||
class="wrn-nav__arrow"
|
||||
data-show="childrenOf(item).length > 0"
|
||||
aria-hidden="true"
|
||||
>›</span>
|
||||
</a>
|
||||
|
||||
<ul
|
||||
class="wrn-nav__submenu"
|
||||
data-wrn-anchored="true"
|
||||
data-show="childrenOf(item).length > 0"
|
||||
>
|
||||
{#each childrenOf(item) as child}
|
||||
<li class="wrn-nav__item" data-has-children='{childrenOf(child).length > 0}'>
|
||||
<a
|
||||
class="wrn-nav__link"
|
||||
href='{child.href || "#"}'
|
||||
data-active='{isActive(child)}'
|
||||
aria-current='{isActive(child) ? "page" : "false"}'
|
||||
aria-disabled='{child.disabled ? "true" : "false"}'
|
||||
@click='choose(child)'
|
||||
>
|
||||
<span
|
||||
class='wrn-nav__icon {child.icon}'
|
||||
data-show="child.icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="wrn-nav__label">{child.label}</span>
|
||||
<span class="wrn-nav__badge" data-show="child.badge">{child.badge}</span>
|
||||
<span
|
||||
class="wrn-nav__arrow"
|
||||
data-show="childrenOf(child).length > 0"
|
||||
aria-hidden="true"
|
||||
>›</span>
|
||||
</a>
|
||||
|
||||
<ul
|
||||
class="wrn-nav__submenu wrn-nav__submenu--level3"
|
||||
data-wrn-anchored="true"
|
||||
data-show="childrenOf(child).length > 0"
|
||||
>
|
||||
{#each childrenOf(child) as leaf}
|
||||
<li class="wrn-nav__item">
|
||||
<a
|
||||
class="wrn-nav__link"
|
||||
href='{leaf.href || "#"}'
|
||||
data-active='{isActive(leaf)}'
|
||||
aria-current='{isActive(leaf) ? "page" : "false"}'
|
||||
aria-disabled='{leaf.disabled ? "true" : "false"}'
|
||||
@click='choose(leaf)'
|
||||
>
|
||||
<span
|
||||
class='wrn-nav__icon {leaf.icon}'
|
||||
data-show="leaf.icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="wrn-nav__label">{leaf.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<slot />
|
||||
</nav>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-nav {
|
||||
--nav-accent: var(--wrn-color-primary);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wrn-nav[data-color="secondary"] {
|
||||
--nav-accent: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-nav[data-color="success"] {
|
||||
--nav-accent: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-nav[data-color="danger"] {
|
||||
--nav-accent: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-nav[data-color="info"] {
|
||||
--nav-accent: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-nav[data-size="sm"] {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wrn-nav[data-size="lg"] {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wrn-nav__list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.wrn-nav[data-orientation="vertical"] .wrn-nav__list {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.wrn-nav__item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wrn-nav__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wrn-nav__link:hover {
|
||||
background: var(--wrn-color-surface-soft);
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wrn-nav__link:focus-visible {
|
||||
outline: 2px solid var(--nav-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-nav__link[data-active="true"] {
|
||||
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
|
||||
color: var(--nav-accent);
|
||||
}
|
||||
|
||||
.wrn-nav__link[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wrn-nav__badge {
|
||||
padding: 0.05rem 0.4rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
|
||||
color: var(--nav-accent);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.wrn-nav__arrow {
|
||||
display: inline-block;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.wrn-nav__item:hover > .wrn-nav__link > .wrn-nav__arrow,
|
||||
.wrn-nav__item:focus-within > .wrn-nav__link > .wrn-nav__arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.wrn-nav__submenu {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 12rem;
|
||||
margin: 0;
|
||||
padding: 0.35rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-md);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 160ms ease;
|
||||
}
|
||||
|
||||
.wrn-nav__item:hover > .wrn-nav__submenu,
|
||||
.wrn-nav__item:focus-within > .wrn-nav__submenu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.wrn-nav__submenu--level3 {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.wrn-nav__toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-nav__toggle-bar {
|
||||
width: 1rem;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
box-shadow:
|
||||
0 -5px 0 currentColor,
|
||||
0 5px 0 currentColor;
|
||||
}
|
||||
|
||||
/*
|
||||
* On a phone the bar becomes a disclosure: submenus stop being floating
|
||||
* overlays and stack inline, because a hover-opened overlay is
|
||||
* unreachable on touch.
|
||||
*/
|
||||
@media (max-width: 767px) {
|
||||
.wrn-nav__toggle {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wrn-nav__list {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.wrn-nav[data-expanded="false"] .wrn-nav__list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrn-nav__submenu,
|
||||
.wrn-nav__submenu--level3 {
|
||||
position: static;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,56 +60,56 @@ size: string = "default"
|
||||
}
|
||||
|
||||
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}">
|
||||
<header class="wrn-navbar wrn-navbar--width-{maxWidth} wrn-component--color-{color} wrn-component--size-{size} {sticky ? 'wrn-navbar--sticky' : ''} {class}" data-wrn-navbar data-open-on-hover="{openOnHover}">
|
||||
<div class="wrn-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}">
|
||||
<div class="wrn-navbar__main">
|
||||
<a class="wrn-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 || ''}" />
|
||||
<img class="wrn-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>
|
||||
<span class="wrn-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if brand.label || brand.description}
|
||||
<span class="wire-navbar__brand-copy">
|
||||
<span class="wrn-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()">
|
||||
<button type="button" class="wrn-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}">
|
||||
<div class="wrn-navbar__collapse {mobileOpen ? 'is-open' : ''}">
|
||||
<nav class="wrn-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
|
||||
{#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' : ''}">
|
||||
<details class="wrn-navbar__dropdown wrn-navbar__dropdown--{item.type || 'dropdown'}" name="wrn-navbar-menu" @toggle="toggleDropdown(event, item)">
|
||||
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
|
||||
{#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>
|
||||
<span class="wrn-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}
|
||||
<div class="wrn-navbar__panel wrn-navbar__panel--columns-{item.columns || 1}">
|
||||
{#if item.description}<p class="wrn-navbar__panel-intro">{item.description}</p>{/if}
|
||||
{#each item.children as child}
|
||||
<div class="wire-navbar__group">
|
||||
<div class="wrn-navbar__group">
|
||||
{#if child.children && child.children.length}
|
||||
{#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
|
||||
{#if child.label}<strong class="wrn-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)">
|
||||
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @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)">
|
||||
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @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>
|
||||
@@ -119,7 +119,7 @@ size: string = "default"
|
||||
</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)">
|
||||
<a class="wrn-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
@@ -127,9 +127,9 @@ size: string = "default"
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="wire-navbar__actions">
|
||||
<div class="wrn-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)">
|
||||
<a class="wrn-navbar__action wrn-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>
|
||||
@@ -140,4 +140,509 @@ size: string = "default"
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
|
||||
style {
|
||||
/* Full application navigation */
|
||||
.wrn-navbar {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
width: 100%;
|
||||
color: var(--wrn-color-text);
|
||||
background: color-mix(in srgb, var(--wrn-color-surface) 96%, transparent);
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wrn-navbar--sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.wrn-navbar__topbar:empty,
|
||||
.wrn-navbar__topbar:not(:has(*)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrn-navbar__topbar:not(:empty) {
|
||||
display: flex;
|
||||
min-height: 2.25rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.35rem clamp(1rem, 4vw, 3rem);
|
||||
color: var(--wrn-color-muted);
|
||||
background: var(--wrn-color-surface-2);
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wrn-navbar__main {
|
||||
display: flex;
|
||||
min-height: 4.25rem;
|
||||
width: min(100%, 90rem);
|
||||
margin-inline: auto;
|
||||
padding: 0.75rem clamp(1rem, 4vw, 3rem);
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.wrn-navbar--width-full .wrn-navbar__main {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wrn-navbar--width-compact .wrn-navbar__main {
|
||||
width: min(86%, 80rem);
|
||||
}
|
||||
|
||||
.wrn-navbar__brand,
|
||||
.wrn-navbar__menu-link,
|
||||
.wrn-navbar__dropdown summary,
|
||||
.wrn-navbar__action,
|
||||
.wrn-navbar__panel a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wrn-navbar__brand {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wrn-navbar__brand-logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 11rem;
|
||||
height: 2.5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.wrn-navbar__brand-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
}
|
||||
|
||||
.wrn-navbar__brand-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.wrn-navbar__brand-copy strong {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wrn-navbar__brand-copy small {
|
||||
margin-top: 0.2rem;
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wrn-navbar__collapse,
|
||||
.wrn-navbar__menus,
|
||||
.wrn-navbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wrn-navbar__collapse {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.wrn-navbar__menus {
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.wrn-navbar__actions {
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wrn-navbar__menu-link,
|
||||
.wrn-navbar__dropdown > summary,
|
||||
.wrn-navbar__action {
|
||||
display: inline-flex;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
border-radius: 0.65rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wrn-navbar__menu-link:hover,
|
||||
.wrn-navbar__dropdown > summary:hover {
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
|
||||
.wrn-navbar__menu-link[aria-current="page"],
|
||||
.wrn-navbar__dropdown > summary[aria-current="page"] {
|
||||
color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
font-weight: 600;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--wrn-component-color, var(--wrn-color-primary)) 12%,
|
||||
transparent
|
||||
);
|
||||
box-shadow: inset 0 -2px 0 var(--wrn-component-color, var(--wrn-color-primary));
|
||||
}
|
||||
|
||||
.wrn-navbar__menu-link[aria-current="page"]:hover,
|
||||
.wrn-navbar__dropdown > summary[aria-current="page"]:hover {
|
||||
color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--wrn-component-color, var(--wrn-color-primary)) 16%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.wrn-navbar__dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wrn-navbar__dropdown > summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.wrn-navbar__dropdown > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrn-navbar__chevron {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: rotate(45deg) translateY(-0.15rem);
|
||||
transition: transform var(--wrn-motion-fast, 150ms) ease;
|
||||
}
|
||||
|
||||
.wrn-navbar__dropdown[open] .wrn-navbar__chevron {
|
||||
transform: rotate(225deg) translate(-0.1rem, -0.1rem);
|
||||
}
|
||||
|
||||
.wrn-navbar__panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.55rem);
|
||||
left: 50%;
|
||||
display: grid;
|
||||
width: max-content;
|
||||
min-width: 15rem;
|
||||
max-width: min(90vw, 64rem);
|
||||
padding: 0.65rem;
|
||||
gap: 0.45rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.9rem;
|
||||
color: var(--wrn-color-text);
|
||||
background: var(--wrn-color-surface);
|
||||
box-shadow: 0 18px 48px rgb(15 23 42 / 0.16);
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0) scale(1);
|
||||
transform-origin: top center;
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
display 180ms allow-discrete;
|
||||
}
|
||||
|
||||
.wrn-navbar__panel::before {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
height: 0.65rem;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.wrn-navbar__dropdown:not([open]) > .wrn-navbar__panel {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
.wrn-navbar__dropdown[open] > .wrn-navbar__panel {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.wrn-navbar__dropdown--mega {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.wrn-navbar__dropdown--mega .wrn-navbar__panel {
|
||||
right: auto;
|
||||
left: 50%;
|
||||
width: min(calc(100vw - 2rem), 64rem);
|
||||
}
|
||||
|
||||
.wrn-navbar__panel--columns-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wrn-navbar__panel--columns-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wrn-navbar__panel--columns-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wrn-navbar__panel-intro {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0.65rem;
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wrn-navbar__group {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.wrn-navbar__group-title {
|
||||
padding: 0.55rem 0.65rem 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wrn-navbar__group > small {
|
||||
padding: 0 0.65rem 0.4rem;
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wrn-navbar__panel a {
|
||||
display: flex;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.65rem;
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
border-radius: 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 450;
|
||||
}
|
||||
|
||||
.wrn-navbar__panel a:hover {
|
||||
background: var(--wrn-color-surface-2);
|
||||
}
|
||||
|
||||
.wrn-navbar__panel a span:last-child {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.wrn-navbar__panel a strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wrn-navbar__panel a small {
|
||||
color: var(--wrn-color-muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wrn-navbar__action {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.wrn-navbar__action--primary {
|
||||
color: var(--wrn-color-on-primary, #fff);
|
||||
background: var(--wrn-component-color, var(--wrn-color-primary));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wrn-navbar__action--outline {
|
||||
border-color: var(--wrn-color-border);
|
||||
}
|
||||
|
||||
.wrn-navbar__toggle {
|
||||
display: none;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
margin-left: auto;
|
||||
padding: 0.65rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: 0.65rem;
|
||||
color: inherit;
|
||||
background: var(--wrn-color-surface);
|
||||
}
|
||||
|
||||
.wrn-navbar__toggle span {
|
||||
display: block;
|
||||
height: 2px;
|
||||
margin: 0.25rem 0;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.wrn-navbar--width-compact .wrn-navbar__main {
|
||||
width: 100%;
|
||||
}
|
||||
.wrn-navbar__toggle {
|
||||
display: block;
|
||||
}
|
||||
.wrn-navbar__collapse {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
padding: 0.75rem 1rem 1rem;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
background: var(--wrn-color-surface);
|
||||
border-bottom: 1px solid var(--wrn-color-border);
|
||||
box-shadow: 0 16px 32px rgb(15 23 42 / 0.12);
|
||||
max-height: calc(100dvh - 4rem);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.wrn-navbar__collapse.is-open {
|
||||
display: flex;
|
||||
}
|
||||
.wrn-navbar__menus,
|
||||
.wrn-navbar__actions {
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.wrn-navbar__menu-link,
|
||||
.wrn-navbar__dropdown > summary,
|
||||
.wrn-navbar__action {
|
||||
width: 100%;
|
||||
}
|
||||
.wrn-navbar__panel,
|
||||
.wrn-navbar__dropdown--mega .wrn-navbar__panel {
|
||||
position: static;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin-top: 0.25rem;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
overflow: hidden;
|
||||
animation: wrn-navbar-mobile-panel 180ms ease both;
|
||||
}
|
||||
.wrn-navbar__dropdown:not([open]) > .wrn-navbar__panel,
|
||||
.wrn-navbar__dropdown[open] > .wrn-navbar__panel {
|
||||
transform: none;
|
||||
}
|
||||
.wrn-navbar__panel[class*="wrn-navbar__panel--columns-"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.wrn-navbar__panel::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.wrn-navbar__brand-copy small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wrn-navbar-mobile-panel {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-0.35rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared component foundation. */
|
||||
|
||||
.wrn-component {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
--wrn-component-scale: 1;
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-family: var(--wrn-font-sans, inherit);
|
||||
}
|
||||
|
||||
.wrn-component--color-primary {
|
||||
--wrn-component-color: var(--wrn-color-primary);
|
||||
}
|
||||
|
||||
.wrn-component--color-secondary {
|
||||
--wrn-component-color: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-component--color-success {
|
||||
--wrn-component-color: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-component--color-warning {
|
||||
--wrn-component-color: var(--wrn-color-warning);
|
||||
}
|
||||
|
||||
.wrn-component--color-danger {
|
||||
--wrn-component-color: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-component--color-info {
|
||||
--wrn-component-color: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-component--size-xs {
|
||||
--wrn-component-scale: 0.78;
|
||||
}
|
||||
|
||||
.wrn-component--size-sm {
|
||||
--wrn-component-scale: 0.88;
|
||||
}
|
||||
|
||||
.wrn-component--size-default,
|
||||
.wrn-component--size-md {
|
||||
--wrn-component-scale: 1;
|
||||
}
|
||||
|
||||
.wrn-component--size-lg {
|
||||
--wrn-component-scale: 1.14;
|
||||
}
|
||||
|
||||
.wrn-component--size-xl {
|
||||
--wrn-component-scale: 1.28;
|
||||
}
|
||||
|
||||
.wrn-component:not(.wrn-btn) {
|
||||
font-size: calc(1em * var(--wrn-component-scale));
|
||||
}
|
||||
|
||||
.wrn-component :is(a, button, input, select, textarea):focus-visible {
|
||||
outline-color: var(--wrn-component-color);
|
||||
}
|
||||
|
||||
.wrn-component p {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,291 @@
|
||||
// Pagination -- page controls over a known total.
|
||||
//
|
||||
// <Pagination page={2} pageSize={10} total={137} variant="numbered" />
|
||||
//
|
||||
// The component owns no data. It reports the requested page through its
|
||||
// change output and lets the caller fetch or slice.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
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 })
|
||||
change(payload: { page: number; pageSize: number })
|
||||
previous(payload: { page: number })
|
||||
next(payload: { page: number })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
size: string = "default"
|
||||
page: number = 1
|
||||
pageSize: number = 10
|
||||
total: number = 0
|
||||
variant: string = "compact"
|
||||
siblingCount: number = 1
|
||||
showSummary: boolean = true
|
||||
label: string = "Pagination"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
previousLabel: string = "Previous"
|
||||
nextLabel: string = "Next"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
shared function lastPage() {
|
||||
var size = Number(pageSize) > 0 ? Number(pageSize) : 10
|
||||
var count = Number(total) > 0 ? Number(total) : 0
|
||||
return Math.max(1, Math.ceil(count / size))
|
||||
}
|
||||
|
||||
// Out-of-range values arrive routinely: page travels as an HTML attribute
|
||||
// and callers compute it from data that may have shrunk underneath them.
|
||||
shared function currentPage() {
|
||||
var value = Number(page)
|
||||
if (!value || value < 1) {
|
||||
return 1
|
||||
}
|
||||
return Math.min(value, lastPage())
|
||||
}
|
||||
|
||||
shared function firstShown() {
|
||||
if (Number(total) < 1) {
|
||||
return 0
|
||||
}
|
||||
return (currentPage() - 1) * (Number(pageSize) || 10) + 1
|
||||
}
|
||||
|
||||
shared function lastShown() {
|
||||
return Math.min(currentPage() * (Number(pageSize) || 10), Number(total) || 0)
|
||||
}
|
||||
|
||||
shared function pageNumbers() {
|
||||
var last = lastPage()
|
||||
var current = currentPage()
|
||||
var siblings = Math.max(0, Number(siblingCount) || 0)
|
||||
var start = Math.max(1, current - siblings)
|
||||
var end = Math.min(last, current + siblings)
|
||||
var pages = []
|
||||
if (start > 1) {
|
||||
pages.push({ value: 1, label: "1", gap: false })
|
||||
if (start > 2) {
|
||||
pages.push({ value: 0, label: "...", gap: true })
|
||||
}
|
||||
}
|
||||
for (var index = start; index <= end; index += 1) {
|
||||
pages.push({ value: index, label: String(index), gap: false })
|
||||
}
|
||||
if (end < last) {
|
||||
if (end < last - 1) {
|
||||
pages.push({ value: 0, label: "...", gap: true })
|
||||
}
|
||||
pages.push({ value: last, label: String(last), gap: false })
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
client function goToPage(target) {
|
||||
var next = Math.min(Math.max(1, Number(target) || 1), lastPage())
|
||||
output.change({ page: next, pageSize: Number(pageSize) || 10 })
|
||||
}
|
||||
|
||||
client function goPrevious() {
|
||||
var target = Math.max(1, currentPage() - 1)
|
||||
output.previous({ page: target })
|
||||
output.change({ page: target, pageSize: Number(pageSize) || 10 })
|
||||
}
|
||||
|
||||
client function goNext() {
|
||||
var target = Math.min(lastPage(), currentPage() + 1)
|
||||
output.next({ page: target })
|
||||
output.change({ page: target, pageSize: Number(pageSize) || 10 })
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
<nav
|
||||
{...attrs}
|
||||
data-ui-component="Pagination"
|
||||
class='wrn-pagination {class}'
|
||||
data-variant='{variant}'
|
||||
data-color='{color}'
|
||||
data-size='{size}'
|
||||
role="navigation"
|
||||
aria-label='{label}'
|
||||
>
|
||||
<p class="wrn-pagination__summary" data-show="showSummary">
|
||||
{firstShown()} to {lastShown()} of {total}
|
||||
</p>
|
||||
|
||||
<div class="wrn-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="wrn-pagination__step"
|
||||
aria-label='{previousLabel}'
|
||||
@click='goPrevious()'
|
||||
>
|
||||
<span class="wrn-pagination__step-icon" aria-hidden="true">‹</span>
|
||||
<span class="wrn-pagination__step-label">{previousLabel}</span>
|
||||
</button>
|
||||
|
||||
<ol class="wrn-pagination__pages" data-show="variant === 'numbered'">
|
||||
{#each pageNumbers() as entry}
|
||||
<li class="wrn-pagination__slot">
|
||||
<span class="wrn-pagination__gap" data-show="entry.gap">{entry.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="wrn-pagination__page"
|
||||
data-show="!entry.gap"
|
||||
data-active='{entry.value === currentPage()}'
|
||||
aria-current='{entry.value === currentPage() ? "page" : "false"}'
|
||||
@click='goToPage(entry.value)'
|
||||
>
|
||||
{entry.label}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
<p class="wrn-pagination__compact" data-show="variant !== 'numbered'">
|
||||
{currentPage()} / {lastPage()}
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wrn-pagination__step"
|
||||
aria-label='{nextLabel}'
|
||||
@click='goNext()'
|
||||
>
|
||||
<span class="wrn-pagination__step-label">{nextLabel}</span>
|
||||
<span class="wrn-pagination__step-icon" aria-hidden="true">›</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</nav>
|
||||
}
|
||||
|
||||
style {
|
||||
.wrn-pagination {
|
||||
--pagination-accent: var(--wrn-color-primary);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wrn-pagination[data-color="secondary"] {
|
||||
--pagination-accent: var(--wrn-color-secondary);
|
||||
}
|
||||
|
||||
.wrn-pagination[data-color="success"] {
|
||||
--pagination-accent: var(--wrn-color-success);
|
||||
}
|
||||
|
||||
.wrn-pagination[data-color="danger"] {
|
||||
--pagination-accent: var(--wrn-color-danger);
|
||||
}
|
||||
|
||||
.wrn-pagination[data-color="info"] {
|
||||
--pagination-accent: var(--wrn-color-info);
|
||||
}
|
||||
|
||||
.wrn-pagination[data-size="sm"] {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wrn-pagination[data-size="lg"] {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wrn-pagination__summary {
|
||||
margin: 0;
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wrn-pagination__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wrn-pagination__pages {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.wrn-pagination__slot {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wrn-pagination__page,
|
||||
.wrn-pagination__step {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 2.25rem;
|
||||
justify-content: center;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius-sm);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrn-pagination__page:hover,
|
||||
.wrn-pagination__step:hover {
|
||||
background: var(--wrn-color-surface-soft);
|
||||
}
|
||||
|
||||
.wrn-pagination__page:focus-visible,
|
||||
.wrn-pagination__step:focus-visible {
|
||||
outline: 2px solid var(--pagination-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wrn-pagination__page[data-active="true"] {
|
||||
border-color: var(--pagination-accent);
|
||||
background: var(--pagination-accent);
|
||||
color: var(--wrn-color-primary-contrast);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wrn-pagination__gap {
|
||||
padding: 0 0.35rem;
|
||||
color: var(--wrn-color-text-muted);
|
||||
}
|
||||
|
||||
.wrn-pagination__compact {
|
||||
margin: 0;
|
||||
padding: 0 0.5rem;
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Below the small breakpoint the word labels crowd the arrows out. */
|
||||
@media (max-width: 639px) {
|
||||
.wrn-pagination {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wrn-pagination__step-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrn-pagination__summary {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user