Compare commits

..
14 Commits
Author SHA1 Message Date
Clintchiz f0447fddb0 fix(db): share registry across bundled copies
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-13 19:10:51 +05:30
Clintchiz 5106256401 fix(production): preserve emitted plugin runtimes 2026-08-13 18:00:30 +05:30
ClintchizandClaude Opus 5 94a6b436f5 test(examples): give each run its own sqlite database
The test profile pointed at ./test.db in the app directory, so a run inherited
whatever schema an earlier run left behind. On a machine with a stale file,
0003_add_password re-applied ALTER TABLE ADD COLUMN over a column that already
existed; fail-on-test-warnings turned the warning into a failure, and
check:production failed for environment reasons rather than code.

Each run now uses a fresh database under the OS temp directory. Verified by
restoring the stale dev.db and test.db that reproduced the failure: the suite
passes with them present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:38:28 +05:30
ClintchizandClaude Opus 5 2425a5146f chore: untrack the .publish staging directory
.publish/ is already listed in .gitignore, but 119 files were committed before
that rule existed, so they stayed tracked. The release prepare step clears and
restages the directory, which meant a routine `git add -A` during a release
would stage the deletion of every package it had not just staged.

Nothing reads the committed contents: publish-packages.ts writes the directory,
test-staged-consumers.mjs reads it after staging, and check-component-imports
skips it. Untracking leaves the release flow unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:37:04 +05:30
ClintchizandClaude Opus 5 fc2c526d70 chore(release): patch-bump csr, db and ui
@wrnexus/csr 0.8.18 -> 0.8.19   output handler errors are now reported
  @wrnexus/db  0.8.10 -> 0.8.11   dead quote tracking removed
  @wrnexus/ui  0.8.15 -> 0.8.16   Typography scoping, Pagination href template

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:10:24 +05:30
ClintchizandClaude Opus 5 fcd552969f chore: drop a stray launch entry and format migrate.ts
An over-broad `git add -A` in the previous commit swept a local dev-server
entry for an unrelated application into .claude/launch.json. It pointed at a
path outside this repository and was not Prettier-formatted, so it failed
format:check. Restored to the version on main.

Also runs Prettier over migrate.ts, which the dead-code removal left unformatted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:03:17 +05:30
ClintchizandClaude Opus 5 5deac48131 chore: refresh the Typography entry in the UI visual contract
The contract tracks a content hash per component source. Rewording the comment
in Typography.wrn changed its hash, so the check failed on an edit that alters
no rendered output. Regenerated intentionally; only that one entry moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:59:52 +05:30
ClintchizandClaude Opus 5 2e1b667285 fix(ui): avoid component-tag syntax in a Typography comment
check-component-imports scans sources for <Component> tags and requires each to
be imported. The explanatory comment added with the prose-scoping fix wrote
<Blockquote> literally, so the checker demanded an import for a component the
file never renders. Naming it without angle brackets keeps the explanation and
clears the gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:58:52 +05:30
ClintchizandClaude Opus 5 8393a953b7 fix(db): drop dead quote tracking in hasExecutableSql, clear lint
hasExecutableSql returns as soon as it meets a quote character, so the quote
variable was assigned and never read: the `if (quote)` branch could never run.
eslint reported it as a useless assignment and the error blocks the release
gate on main. Removing the variable and the unreachable branch keeps behaviour
identical, since encountering a quote already means the SQL is executable.

Also drops an eslint-disable directive in csr's output error reporter that
suppressed nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:57:34 +05:30
ClintchizandClaude Opus 5 2459a0b5a7 chore: sync bun.lock with the @wrnexus/ui package version
The lockfile recorded @wrnexus/ui at 0.8.13 while its manifest is 0.8.15, so
validate:0.8 failed its workspace version check. The drift predates this branch
and is present on main; bun install does not rewrite workspace metadata that is
already satisfied, so this applies the same targeted rewrite the release
tooling performs in syncWorkspaceLock. No version is bumped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:54:34 +05:30
ClintchizandClaude Opus 5 4e5949aedc chore: refresh the public API snapshot
The committed snapshot had drifted from source on main: dev-server exports
validateRpcCsrf, and styles exports the browser-cookie configuration types and
resolveBrowserCookieOptions. All six are additions, so the surface stays
backward compatible.

This unblocks check:production, which fails on main for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:51:22 +05:30
ClintchizandClaude Opus 5 60e38b983c build(editor): rebuild the bundled compiler and language server
The checked-in bundles predated the typecheck fix that resolves @wrnexus/ui
imports as component contracts, so VS Code reported "has no exported member"
for components such as Grid while the CLI typechecker passed. The package
exports nothing by design — components resolve by directory scan — so the
stale bundle was the whole defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:47:08 +05:30
ClintchizandClaude Opus 5 5afb2d1875 fix(ui): make Pagination usable server-rendered, surface output errors
Pagination reported the requested page only through its change output, so on a
server-rendered page the controls did nothing: the parent had to own client
state to react, and the emitted page never became a URL. An optional
hrefTemplate now renders the steps and page numbers as anchors, which work
before hydration and without JavaScript and give each page a crawlable URL.
Buttons remain the default for client-owned lists. End steps are clamped and
marked disabled rather than linking past the first or last page.

Output handler errors are no longer swallowed. Nothing awaits invokeOutput, so
a handler that threw became an unhandled rejection that never reached the
console and presented as a control that silently does nothing. Handler errors
are now reported as WRN-DEV-OUTPUT-HANDLER-ERROR.

Regenerates the component reference and the UI visual contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:46:00 +05:30
ClintchizandClaude Opus 5 2e42be7b31 fix(ui): stop Typography prose styles leaking into nested components
Typography styled every descendant element, including those inside nested UI
components. A <Blockquote variant="bordered"> placed inside <Typography> drew
two left borders: its own, plus the one these prose rules apply to any
<blockquote>. The same leak applied to headings, links, lists and code.

Prose rules now exclude the subtree of any nested `.wrn-component`, so they
style raw markup only. The exclusion sits inside :where() so specificity stays
at zero and applications can still override these rules with a plain class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:09:46 +05:30
145 changed files with 438 additions and 51262 deletions
-197
View File
@@ -1,197 +0,0 @@
# @wrnexus/ai
Provider-neutral AI orchestration for OpenAI, Anthropic, Google and local OpenAI-compatible models,
with streaming, structured output, tools, embeddings, vector search/RAG, conversation persistence,
templates, guardrails, usage events, fallback, rate limits and evaluation reports.
> A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/ai` is a thin, dependency-free wrapper over the Anthropic **Messages API**,
built on `fetch` (Bun-native, no SDK). Use it in API routes, jobs, or middleware to
call Claude. It defaults to the most capable model, **`claude-opus-4-8`**, reads your
key from `ANTHROPIC_API_KEY`, and supports both one-shot generation and streaming.
## Installation
```bash
bun add @wrnexus/ai
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
Set your key in the environment (e.g. `.env`):
```
ANTHROPIC_API_KEY=sk-ant-...
```
## API
### `createAI(config?)`
Creates a client. The key is read at call time, so it's safe to create at import.
```ts
import { createAI } from "@wrnexus/ai";
const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version })
```
`AIConfig` fields (all optional):
| Field | Default | Description |
| ----------- | --------------------------- | -------------------------- |
| `apiKey` | `ANTHROPIC_API_KEY` | Anthropic API key |
| `model` | `"claude-opus-4-8"` | Model id |
| `maxTokens` | `4096` | Default max output tokens |
| `baseURL` | `https://api.anthropic.com` | API base URL |
| `version` | `"2023-06-01"` | `anthropic-version` header |
### `ai.generate(prompt, opts?): Promise<string>`
One-shot text generation. `prompt` is a string or a `Message[]` history.
```ts
const text = await ai.generate("Write a haiku about Bun.");
const reply = await ai.generate(
[
{ role: "user", content: "My name is Ada." },
{ role: "assistant", content: "Hi Ada!" },
{ role: "user", content: "What's my name?" },
],
{ system: "You are concise." },
);
```
### `ai.stream(prompt, opts?): AsyncGenerator<string>`
Yields text deltas as they arrive.
```ts
for await (const chunk of ai.stream("Tell me a story.")) {
process.stdout.write(chunk);
}
```
### `ai.streamResponse(prompt, opts?): Response`
Returns a streaming `text/plain` `Response` — drop it straight into an API route.
```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI();
export const POST = async (ctx) => {
const { prompt } = await ctx.req.json();
return ai.streamResponse(prompt);
};
```
### `GenerateOptions`
| Option | Type | Description |
| ----------- | ------------------------------------------------- | ---------------------------------------------------- |
| `system` | `string` | System prompt |
| `model` | `string` | Override the model for this call |
| `maxTokens` | `number` | Override max output tokens |
| `thinking` | `boolean` | Enable adaptive extended thinking (deeper reasoning) |
| `effort` | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | Reasoning effort / token spend |
| `messages` | `Message[]` | Full history — supersedes `prompt` |
| `signal` | `AbortSignal` | Cancel the request |
> `temperature` / `top_p` are intentionally **not** exposed — the current Claude
> models reject them (400). Steer output with prompting instead.
### `AIError`
Thrown on non-2xx responses or a model refusal. Carries `.status` and `.type`
(e.g. `"authentication_error"`, `"rate_limit_error"`, `"refusal"`).
```ts
import { AIError } from "@wrnexus/ai";
try {
await ai.generate("...");
} catch (e) {
if (e instanceof AIError && e.type === "rate_limit_error") {
/* back off */
}
}
```
### Multi-provider client
`createAIClient` adds named-provider selection and fallback, capability discovery,
validated JSON output, validated tool execution, abort-aware exponential retries,
and per-provider circuit breakers. Attempt events intentionally contain metadata
only: prompts, credentials, and raw model responses are never passed to telemetry.
```ts
import { anthropicProvider, createAIClient } from "@wrnexus/ai";
const ai = createAIClient({
providers: [anthropicProvider()],
retry: { attempts: 3, baseDelayMs: 100, maxDelayMs: 2_000 },
circuitBreaker: { failureThreshold: 5, resetAfterMs: 30_000 },
});
const result = await ai.generateObject<{ title: string }>("Return a JSON title", {
validate: (value): value is { title: string } =>
typeof value === "object" && value !== null && "title" in value,
});
```
Providers can return normalized `usage` (`inputTokens`, `outputTokens`,
`totalTokens`, and `costUsd`) and `toolCalls`. Use `executeTools` with a named,
validated tool registry; unknown tools and invalid arguments are rejected before
application code runs. `deterministicAIProvider` supplies ordered or computed
offline responses for tests and examples without API keys or network calls.
## Usage
### Return generated JSON from an API route
```ts
// app/api/summarize.ts — summarize posted text
import { createAI } from "@wrnexus/ai";
const ai = createAI();
export const POST = async (ctx) => {
const { text } = await ctx.req.json().catch(() => ({}));
if (!text) return Response.json({ error: "Provide 'text'." }, { status: 400 });
const summary = await ai.generate(`Summarize in one sentence:\n\n${text}`, {
system: "You are a precise summarizer.",
});
return Response.json({ summary });
};
```
### Stream a chat response to the browser
```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI({ model: "claude-sonnet-5" });
export const POST = async (ctx) => {
const { messages } = await ctx.req.json();
return ai.streamResponse(messages, {
system: "Answer using concise Markdown.",
maxTokens: 1_500,
});
};
```
## Requirements / Notes
- **Bun-only.** Uses `fetch`, `ReadableStream`, `TextDecoder`/`TextEncoder`, and
reads `ANTHROPIC_API_KEY` from `Bun.env` (falls back to `process.env`).
- **Zero dependencies** — no `@anthropic-ai/sdk`; talks to the Messages API directly.
- Defaults to `claude-opus-4-8`. Pass `{ model }` for a different model (e.g.
`"claude-sonnet-5"` for speed/cost, `"claude-haiku-4-5"` for the fastest).
-47
View File
@@ -1,47 +0,0 @@
{
"name": "@wrnexus/ai",
"version": "0.8.9",
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/ai"
},
"homepage": "https://wrnexusjs.dev/packages/ai",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"ai"
],
"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"
},
"./platform": {
"types": "./dist/platform.d.ts",
"import": "./dist/platform.js"
}
},
"files": [
"dist",
"README.md"
]
}
-304
View File
@@ -1,304 +0,0 @@
# @wrnexus/authz
> Composable authorization for WrNexus — role-based (RBAC), policy-based (PBAC), and attribute-based (ABAC) access control that reduces to a boolean check plus an `authorize()` guard.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/authz` is a small, server-side authorization toolkit. It gives you three
interchangeable models — RBAC (roles → permissions), PBAC (policy predicates), and
ABAC (attribute matchers) — that all collapse to a `boolean | Promise<boolean>` decision.
Wrap any decision in a `Middleware` guard (`authorize`, `requireRole`, `requirePermission`)
to protect WrNexus routes. Reach for it whenever a route or action needs to be gated on who
the user is, what roles they hold, or attributes of the user and the resource. It plugs into
`@wrnexus/core` by reading `ctx.user` as the authorization subject.
## Installation
```bash
bun add @wrnexus/authz
```
> 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 (`@wrnexus/authz`) exporting the following.
### Types
| Symbol | Description |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `Subject` | The authorized principal: `{ id?: string; roles?: string[]; [attribute: string]: unknown }`. |
| `Rbac` | An RBAC checker: `{ can(subject, permission): boolean; permissionsFor(roles): Set<string> }`. |
| `Policy<S = Subject, R = unknown>` | A predicate `(subject: S, resource?: R) => boolean \| Promise<boolean>`. |
### RBAC
#### `defineRbac(roles: Record<string, string[]>): Rbac`
Builds an RBAC checker from a role → permissions map. Supported permission forms:
- `"*"` — grants every permission.
- `"ns:*"` — namespace wildcard (e.g. `"post:*"` grants `"post:write"`).
- `"role:<name>"` — inherits all permissions of another role (resolved recursively, cycle-safe).
The returned `Rbac` provides:
- `can(subject, permission)``true` if any of `subject.roles` grants `permission` (honouring `*` and namespace wildcards). Returns `false` when the subject has no roles.
- `permissionsFor(roles)` — the resolved `Set<string>` of all permissions granted to a set of roles.
#### `hasRole(subject: Subject | undefined, ...required: string[]): boolean`
`true` if the subject holds **all** of the given roles.
### PBAC / ABAC combinators
- `any<S, R>(...policies: Policy<S, R>[]): Policy<S, R>` — allow if **any** policy passes (OR); awaits async policies.
- `all<S, R>(...policies: Policy<S, R>[]): Policy<S, R>` — allow only if **all** policies pass (AND); awaits async policies.
- `attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S>` — ABAC helper that allows when `subject[name]` equals `match`, or when `match` is a function, when `match(value)` is truthy.
### Guards (middleware)
Each guard returns a `@wrnexus/core` `Middleware`. A denied request short-circuits with
`Response.json({ ok: false, error: "Forbidden" }, { status: 403 })`.
- `authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware` — runs `policy` against the request `Context`; calls `next()` when it resolves truthy, otherwise returns 403.
- `requireRole(...roles: string[]): Middleware` — allows when `ctx.user` holds **any** of the listed roles.
- `requirePermission(rbac: Rbac, permission: string): Middleware` — allows when `rbac.can(ctx.user, permission)` is `true`.
## Usage
### RBAC
```ts
import { defineRbac, hasRole } from "@wrnexus/authz";
const rbac = defineRbac({
admin: ["*"],
editor: ["post:read", "post:write"],
viewer: ["post:read"],
// role inheritance: lead gets everything an editor has, plus post:publish
lead: ["role:editor", "post:publish"],
});
const user = { id: "u1", roles: ["editor"] };
rbac.can(user, "post:write"); // true
rbac.can(user, "post:delete"); // false
rbac.permissionsFor(["lead"]); // Set { "post:read", "post:write", "post:publish" }
hasRole(user, "editor"); // true
```
### Guarding routes
```ts
import { authorize, requireRole, requirePermission, defineRbac } from "@wrnexus/authz";
const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });
// Only admins or editors
app.get("/dashboard", requireRole("admin", "editor"), handler);
// Requires a specific permission
app.post("/posts", requirePermission(rbac, "post:write"), handler);
// Arbitrary policy over the request context
app.delete(
"/posts/:id",
authorize((ctx) => hasRole(ctx.user, "admin")),
handler,
);
```
### PBAC / ABAC policies
```ts
import { any, all, attr, authorize, type Policy } from "@wrnexus/authz";
interface User {
id: string;
department?: string;
roles?: string[];
}
interface Post {
authorId: string;
}
// Ownership policy (subject + resource)
const ownsPost: Policy<User, Post> = (u, post) => u.id === post?.authorId;
// ABAC: attribute equality, or a predicate
const inEngineering = attr<User>("department", "engineering");
const isVerified = attr<User>("verified", (v) => v === true);
// Compose: allow if the user owns the post OR is in engineering AND verified
const canEdit = any(ownsPost, all(inEngineering, isVerified));
app.put(
"/posts/:id",
authorize((ctx) => canEdit(ctx.user as User, loadPost(ctx))),
handler,
);
```
## Requirements / Notes
- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported.
- Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`.
- Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise<boolean>` (e.g. for a database ownership check).
## Declaring permissions
The RBAC/PBAC/ABAC surface above is the low-level toolkit. On top of it sits a
declarative **registry + catalog + store + engine**: permissions, roles, and
policies are declared once in code, merged into a frozen catalog at boot, and
resolved per-request against a pluggable `PermissionStore` that holds who has
what.
Put declarations in `app/authz/<name>.ts`; they are discovered automatically
and merged (conflicting declarations of the same permission/role/policy across
files fail the boot loudly, naming both source files).
```ts
import { defineAuthz, owner } from "@wrnexus/authz";
export default defineAuthz({
permissions: {
"post:read": { title: "View posts", public: true },
"post:delete": { title: "Delete posts", risk: "high" },
},
// "post:*" is a namespace wildcard grant, valid inside a role's list — it is
// not itself a registered permission, so it can only ever grant permissions
// that ARE declared above (e.g. "post:read", "post:delete").
roles: { editor: ["post:*"], admin: ["role:editor"] },
policies: { ownsPost: owner("id", "authorId") },
bindings: { "post:delete": ["ownsPost"] },
});
```
`public: true` means anonymous callers may hold the permission — but any
policy bound to it still runs, and can still veto the anonymous caller (e.g. a
`notBanned` policy on a public `post:preview` permission).
## Checking permissions
Register `authzMiddleware` once, in `app/middleware/`, with the merged
catalog and a `PermissionStore`. Like every other `app/middleware/*.ts` file,
the registration is an eager, module-scope call — the same shape as
`authzMiddleware({ catalog, store })` requires — so it must run after the
catalog has been populated. Both the dev server and `wrnexus build`'s
generated production entry guarantee `getAuthzCatalog()` is populated before
any app middleware module evaluates. Name the file so it sorts after whatever
middleware sets `ctx.user` (middleware runs in alphabetical filename order —
`authz.ts` after `auth.ts`, for instance).
```ts
// app/middleware/authz.ts
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
import { dbPermissionStore } from "@wrnexus/authz/db";
import { getDb } from "@wrnexus/db";
export default authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) });
```
> **`subject.id` must be a non-empty string.** The engine denies (and logs to
> stderr) whenever `ctx.user.id` is present but not a non-empty string — this
> includes the common case of an integer primary key. Coerce it before it
> reaches `ctx.user`, e.g. `user.id = String(row.id)`, or every request for
> that user denies with "Invalid subject" instead of resolving normally.
> `owner()` (the built-in ownership policy) compares subject and resource ids
> with `Object.is`, so both sides must be the same type too — `owner()` on a
> numeric `resource.authorId` against a stringified `subject.id` never
> matches even when they represent "the same" id.
There is no per-route `middleware` export — `app/middleware/*.ts` is the only
place middleware is registered. To gate part of the app, branch on the
request the same way any other conditional middleware does (compare
`app/middleware/captcha-login.ts` in the auth showcase, which branches on
method + path the same way):
```ts
// app/middleware/protect-posts.ts
import type { Context, Next } from "@wrnexus/core";
import { guardPermission } from "@wrnexus/authz";
const guardPostWrite = guardPermission("post:write");
export default function protectPosts(ctx: Context, next: Next) {
return ctx.url.pathname.startsWith("/api/posts") && ctx.req.method !== "GET"
? guardPostWrite(ctx, next)
: next();
}
```
Or check inline inside a route handler with the free function `can()`:
```ts
// app/api/posts/[id].ts
import type { Context } from "@wrnexus/core";
import { can } from "@wrnexus/authz";
export const DELETE = async (ctx: Context) => {
const post = { id: "1", authorId: "alice" }; // load your own resource here
if (!(await can(ctx, "post:delete", post))) {
return Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
}
return Response.json({ ok: true });
};
```
`can()` is a free function taking `ctx`, not `ctx.can``@wrnexus/core` must
not depend on `@wrnexus/authz`, so the per-request resolver lives in
`ctx.locals` instead, reached through `can()` / `decideFor()` /
`guardPermission()` / `filterCan()`. Calling any of them before
`authzMiddleware` has run for that request throws a `WRN-AUTHZ-SETUP` error
naming the missing registration, rather than silently denying.
See `examples/auth-showcase/app/authz/showcase.ts` and
`examples/auth-showcase/app/middleware/authz.ts` for a complete, runnable
version of this wiring.
## Precedence
1. An explicit deny wins over everything, including `*` — and honours the
same namespace-wildcard matching as grants (denying `post:*` blocks
`post:comment:delete`, not just `post:*` itself).
2. A bound policy can veto a permission a role grants, and runs even for a
`public: true` permission — including for an anonymous caller.
3. Otherwise the permission must be held via a role or an explicit grant.
4. Default deny.
Every failure — an unknown permission (outside strict/dev mode), a store
outage, a thrown policy — denies rather than throwing through to the caller.
`permissionsFor()` (on the resolver returned by `createAuthzResolver`) is a
coarse hint for hiding UI (e.g. a menu section), **never authoritative**. A
`Set<string>` cannot represent "granted `post:*` except `post:delete`", so a
narrow deny beneath a broad grant is invisible to it — the set still contains
`post:*` while `can()` / `decide()` correctly refuse `post:delete`. Gate real
actions with `can()`, `decideFor()`, or `filterCan()`; never by matching
against `permissionsFor()`'s result.
## CLI
```bash
wrnexus authz list # every registered permission, role, and policy
wrnexus authz generate # app/authz/permissions.gen.ts type unions
wrnexus authz init # scaffold the assignment-table migration
```
`wrnexus authz generate`'s output is a plain `Permission | Role` string-literal
union — `can()`, `guardPermission()`, and `decideFor()` all take a bare
`string` and nothing reads this file automatically, so import it to type your
own helpers/constants against the registered catalog, e.g.:
```ts
import type { Permission } from "app/authz/permissions.gen.ts";
function guard(permission: Permission) {
return guardPermission(permission);
}
```
-51
View File
@@ -1,51 +0,0 @@
{
"name": "@wrnexus/authz",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/authz"
},
"homepage": "https://wrnexusjs.dev/packages/authz",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"authz"
],
"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"
},
"./db": {
"types": "./dist/db.d.ts",
"import": "./dist/db.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.9",
"@wrnexus/db": "^0.8.9"
},
"files": [
"dist",
"README.md"
]
}
-326
View File
@@ -1,326 +0,0 @@
# @wrnexus/cli
Production parity commands:
```bash
wrnexus build .
wrnexus preview . --port=3000
wrnexus dev . --production-runtime
```
`preview` refuses to start without `dist/server.js` and executes that exact
artifact with the production profile. Production-runtime development rebuilds
the same minified artifact after app, public, or configuration changes and
keeps the last good server running when a rebuild fails.
> The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/cli` provides the `wrnexus` executable — the single entry point for developing a WrNexus app. It runs the HMR dev server, produces a self-contained production build, scaffolds apps/pages/components, drives database migrations, regenerates typed routes and queries, runs tests, and manages configuration profiles. It also scaffolds multi-app monorepos and serves them behind a domain-routing gateway. This is a CLI/build-time package (it shells out to the Bun binary for the dev child and tests) and it also exports the workspace config types via a subpath.
## Installation
```bash
bun add @wrnexus/cli
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
Once installed, invoke it from an app directory:
```bash
bunx wrnexus dev
# or add scripts: "dev": "wrnexus dev .", "build": "wrnexus build ."
```
## Commands
### Local production services
`wrnexus dev . --services` starts the application and the bounded local database,
cache, mail, SMS, webhook, storage, queue, cron, authentication and metrics simulator.
It generates a localhost/`*.localhost` development certificate under
`.wrnexus/certificates/` and serves both the application and service console over HTTPS.
Trust that certificate locally to remove the browser warning. Use `--services-http` only
when an external development proxy already terminates TLS.
### Exact production runtime with live updates
`wrnexus dev . --production-runtime` rebuilds and executes `dist/server.js` with
production resolution, serialization, caching, headers and assets. The supervisor keeps
the last good process when a build fails. On a successful rebuild the opt-in production
HMR socket reconnects, requests the new document and morphs it into the browser; ordinary
`wrnexus preview` and deployed production servers never include that client.
### API platform
`wrnexus api generate [app-dir]` (or `api docs`) derives operations from file routes and
emits `generated/api/openapi.json`, safe static documentation, Postman collection, curl examples,
and TypeScript, JavaScript, Java, Go and Python SDKs. Generate one client with
`wrnexus sdk generate <language> [app-dir]`.
Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=<name>` (see [Profiles](#profiles)).
| Command | Purpose |
| ------------------------------------- | ---------------------------------------------------------------------- |
| `wrnexus dev [app-dir] [--port=3000]` | Start the development server with live reload / HMR. |
| `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. |
| `wrnexus create <app-name>` | Scaffold a new single app from an inline template. |
| `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). |
| `wrnexus workspace add <name>` | Add and register an app in the current workspace. |
| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. |
| `wrnexus production [workspace-dir]` | Build, migrate, and serve every workspace app in production. |
| `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. |
| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). |
| `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 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. |
| `wrnexus compatibility check` | Check whether behavior defaults are explicitly pinned and current. |
| `wrnexus compatibility explain` | Explain configured, effective, and current compatibility behavior. |
| `wrnexus compatibility upgrade` | Back up config and explicitly opt into reviewed current behavior. |
| `wrnexus help` | Print usage. |
`wrnexus g` is an alias for `wrnexus generate`.
Compatibility upgrades never happen implicitly. New applications pin
`compatibilityDate` and `frameworkBehaviour`; existing applications use
`wrnexus compatibility explain` before the backed-up, idempotent upgrade command.
### `wrnexus dev`
Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`).
```bash
wrnexus dev . --port=8080
```
### `wrnexus build`
Emits into `<app-dir>/dist/`:
- `server.js` — a single, minified, self-contained Bun server with a **static** manifest of every page / api / realtime / middleware / component / layout module (no runtime filesystem scan or on-the-fly bundling).
- `reactive.js`, `theme.css`, `theme.js`, `ui.css`, and (if present) `styles.css` — hashed, minified browser assets.
- `public/` — copied verbatim.
Before bundling, it regenerates typed queries for the default and every named database. Run the output with:
```bash
bun dist/server.js # PORT env var optional
# Generated apps also provide: npm start
# Build and start together: npm run production
```
### `wrnexus create`
Scaffolds a complete v0.8 app from an inline template. The generated project includes strict TypeScript, ESLint and Prettier, editor recommendations, environment templates, database migrations, locales, schemas, tests, API/middleware/realtime examples, Tailwind and Iconify, PWA/mobile defaults, and the framework package kits. Its `wrnexus.config.ts` documents the current imports, types, stores, performance, observability, tenancy, build, navigation, theme, i18n, database, storage, realtime, security, and profile configuration.
Use `bun run dev` during development, `bun run check` for the complete typecheck/lint/test/format gate, `bun run build && bun run start` for production, or `bun run production` to build and start in one command.
### `wrnexus update`
`wrnexus update --latest` performs a complete project upgrade. It hands control to the exact target CLI, backs up important project files under `.wrnexus/update-backups/`, updates every `@wrnexus/*` dependency, refreshes framework-owned references, and applies every versioned syntax/config/file migration between the project version and target version. After installation it runs the project's `check` and `build` scripts; the new version is recorded only after verification succeeds.
Use `--dry-run` to preview an upgrade or `--no-verify` when verification is intentionally handled elsewhere. Migrations never overwrite user-owned configuration wholesale: each release must provide a focused, idempotent transformation for any changed syntax or config contract.
```bash
wrnexus create my-app
```
### `wrnexus generate`
Scaffolds a single file from a template, refusing to overwrite an existing file. Types (with aliases): `page`/`p`, `component`/`c`, `api`/`a`, `schema`/`s`. Nested names create nested paths.
```bash
wrnexus generate page about # app/pages/about.wrn
wrnexus generate component user-card # app/components/user-card.wrn
wrnexus generate api users/list # app/api/users/list.ts
wrnexus generate schema signup # app/schemas/signup.ts
wrnexus generate routes # regenerate app/routes.gen.ts
wrnexus generate docker # Dockerfile + compose + .dockerignore
wrnexus generate mobile --mode=webview --app-id=com.example.app --app-name="Example" --url=https://app.example.com
wrnexus generate mobile --mode=native
```
The mobile generator creates a separate `mobile/` package and reads
`config.mobile.mode`. `webview` creates a Capacitor shell that renders the hosted
WrNexus application. `native` creates a WebView-free Expo/React Native app whose
screens call the shared backend through `mobile/src/wrnexus.ts`. Native screens
do not render `.wrn` HTML. In either mode, run `bun install` in `mobile/`; iOS
device builds require macOS and Xcode.
Install official or community Capacitor plugins through the root CLI:
```bash
wrnexus mobile add @capacitor/camera @capacitor/haptics
wrnexus mobile sync
wrnexus mobile assets # generate native icons from config.mobile.icon
```
In native mode, `mobile add` runs `expo install` and `mobile sync` runs Expo
prebuild. In WebView mode they retain the Capacitor install/sync behavior.
`wrnexus mobile compile` maps portable `app/pages/**/*.wrn` pages to Expo Router
TSX routes. Native `bun run start` invokes this compilation automatically.
Browser code can access installed plugins through the SSR-safe
`@wrnexus/mobile` bridge. The command adds each plugin to both the WrNexus app
(JavaScript proxy) and `mobile/` (native synchronization).
`wrnexus mobile sync` also configures Android so only true network failures use
the local connection-error screen. HTTP errors such as 404 and 500 keep their
WrNexus response pages.
### `wrnexus eject`
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
```
### `wrnexus db`
Database migrations and tooling. Without a flag, commands target the **default** database (`db` in `wrnexus.config.ts`, files under `app/db/`). Pass `--db=<name>` to target a named database (`databases.<name>`, files under `app/db/<name>/`).
| Subcommand | Purpose |
| ------------------------------- | ----------------------------------------------------------------------------------- |
| `db new <name> [--from-models]` | Scaffold a migration; `--from-models` derives it from the TS models in `schema.ts`. |
| `db migrate` | Apply all pending migrations. |
| `db rollback` | Revert the last applied migration. |
| `db status` | List applied / pending migrations. |
| `db generate` | Regenerate typed queries (`queries/*.sql``queries.gen.ts`). |
| `db seed` | Run the database's `seed.ts` (default export / `seed` function). |
| `db studio [table]` | Inspect tables — list row counts, or dump the first 50 rows of one table. |
```bash
wrnexus db new create_users --from-models
wrnexus db migrate
wrnexus db studio users
wrnexus db status --db=analytics
```
### `wrnexus workspace` and `wrnexus gateway`
`workspace <name>` scaffolds a monorepo: complete v0.8 apps under `apps/*`, shared libraries under `packages/*`, root TypeScript/lint/format/editor/environment tooling, and a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). Newly added workspace apps use the same current scaffold.
```bash
wrnexus workspace acme
wrnexus gateway --port=3000
```
For a complete production start, use the first-class workspace orchestrator:
```bash
wrnexus production --host=0.0.0.0 --port=3000
```
It builds every registered app, applies default and named-database SQL migrations
when present, and starts the production gateway only after preparation succeeds.
Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are
managed separately; `--environment=<name>` selects another workspace environment.
From a workspace root, add and register another app in one command:
```bash
wrnexus workspace add reports --domain=reports.localhost
bun install
```
Development gateways bind to `127.0.0.1` by default for reliable access on Windows,
macOS, and Linux. Open the configured app domain on the gateway port (for example
`http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports
printed while apps start. Pass `--host=0.0.0.0` to accept connections from other devices.
### `wrnexus test`
Runs the app's tests with `bun test`. Defaults to the `test` profile (config + `.env.test`). Pass `--watch` to re-run on change; extra flags pass straight through to `bun test`.
```bash
wrnexus test . --watch
```
## Usage
### Create and run a single application
```bash
bunx @wrnexus/cli create customer-portal
cd customer-portal
bun install
bun run dev
```
### Add routes and shared UI to an existing app
```bash
wrnexus generate page reports/monthly
wrnexus generate api reports/export
wrnexus generate component report-filter
wrnexus generate routes
```
### Create a multi-app workspace and add another app
```bash
wrnexus workspace company-suite
cd company-suite
wrnexus workspace add reports --domain=reports.localhost
bun install
wrnexus gateway --port=3000
```
Open `http://reports.localhost:3000`; the gateway selects `apps/reports` from the
request host.
### Upgrade with migrations and verification
```bash
wrnexus update --latest --dry-run
wrnexus update --latest
wrnexus doctor
```
Use `wrnexus doctor --fix [app-dir]` to apply conservative repairs before the
health check: create missing `app/pages` and a default config, align skewed
`@wrnexus/*` dependency ranges, record the current migration marker, and format
only syntax-valid `.wrn` files. Invalid JSON or WRN sources are reported/skipped
instead of overwritten; repeat runs are idempotent.
## Profiles
Pass `--profile=<name>` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.<profile>`, `.env.<profile>.local`) into `process.env`.
```bash
wrnexus dev --profile=uat
wrnexus profiles # ● development (config, .env.development)
# ○ production
# ○ uat (config, .env.uat)
```
## Subpath exports
`@wrnexus/cli/workspace` exposes the workspace configuration types used by `wrnexus.workspace.ts`:
```ts
import type { WorkspaceConfig, WorkspaceApp } from "@wrnexus/cli/workspace";
const config: WorkspaceConfig = {
security: { trustedHostsOnly: true, headers: true, accessLog: true },
apps: [{ name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] }],
};
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 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.
-70
View File
@@ -1,70 +0,0 @@
{
"name": "@wrnexus/cli",
"version": "0.8.17",
"type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/cli"
},
"homepage": "https://wrnexusjs.dev/packages/cli",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"cli"
],
"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"
},
"./workspace": {
"types": "./dist/workspace.d.ts",
"import": "./dist/workspace.js"
}
},
"bin": {
"wrnexus": "./dist/index.js"
},
"dependencies": {
"@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": [
"dist",
"README.md"
]
}
-217
View File
@@ -1,217 +0,0 @@
# @wrnexus/compiler
## Partial-static rendering
Pages can select `render = "partial-static"` and divide their view with `<Static>` and
`<Dynamic>` boundaries. The compiler emits a build-only shell renderer that never evaluates
dynamic-boundary children. `wrnexus build` expands static component mounts into
`dist/partial-shells.json`, records byte/region evidence in `build-report.json`, and embeds
the shell in the production route manifest. At request time the production runtime retains
request-aware layouts, locale/theme metadata and security nonces while streaming dynamic
regions into stable placeholders.
> Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
Production adapters use `analyzeRuntimeImports` before bundling. Edge, worker,
service-worker, and browser targets reject Node filesystem, TCP, and process
modules with `WRN-RUNTIME-CAPABILITY`. Package manifests can declare supported
`wrnexus.runtimes` and required `wrnexus.requires` capabilities; discovery fails
when the selected deployment cannot satisfy them.
## Server actions
```wrn
action createUser using CreateUserSchema {
const user = await users.create(input)
invalidate("users")
return user
}
view {
<form @submit="createUser">...</form>
}
```
The compiler produces a schema-aware server registry, a fully inferred action
client, and progressively enhanced form metadata. The shared runtime performs
validation, authentication/permission checks, CSRF verification, serialization,
invalidation reporting, and browser lifecycle events.
## Overview
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
Static ES module imports may appear before the root declaration. Imported values are
available to server-rendered expressions, including component props:
```wrn
import { appUrl } from "@wrnexus/helpers";
layout PublicLayout {
view {
<PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
}
}
```
## Installation
```bash
bun add @wrnexus/compiler
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
All exports come from the package root (`@wrnexus/compiler`).
### `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.
### `compile(source: string): CompileResult`
Richer entry point that returns the generated code, the AST, and any diagnostics.
```ts
interface CompileResult {
code: string;
ast: PageAst;
diagnostics: string[];
}
```
On a `ParseError` it pushes the message into `diagnostics` and re-throws.
### `parse(source: string): PageAst`
Run the lexer + recursive-descent parser and return the AST. Throws `ParseError` (lexer `LexError`s are caught and rethrown as `ParseError`).
### `generate(ast: PageAst): string`
Lower a `PageAst` to TypeScript. `page` ASTs become a default-export page component (plus `meta`, optional `layout`, `__wrnexusApi`/method handlers, `websocket`, and SSR/CSR data bindings); `component` ASTs become a module exporting `render(props)` and `__wrnexusComponent`.
### `Lexer`
On-demand lexer for `.wrn`. Yields structural tokens and exposes raw-span readers for the parser.
```ts
class Lexer {
pos: number;
constructor(src: string);
next(): Token; // consume next structural token
peek(): Token; // look ahead without consuming
readPath(): string; // route path, e.g. /users/[id]
readToLineEnd(): string; // rest of line (state/prop initializers)
readBalancedBraces(): string; // inner text of a { ... } block, string-aware
}
```
`Token` is `{ type: TokenType; value: string; pos: number }`, where `TokenType` is one of `ident`, `string`, `lbrace`, `rbrace`, `lparen`, `rparen`, `at`, `eq`, `comma`, `eof`.
### Errors
| 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
Exported type-only symbols describing the parsed tree:
| Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst` | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. |
| `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. |
| `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. |
| `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. |
| `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. |
| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. |
| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. |
| `DataMode` | `"ssr" \| "client"`. |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. |
## Usage
Compile a page:
```ts
import { compileWrnFile } from "@wrnexus/compiler";
const ts = compileWrnFile(`
page Home {
state count = 0
seo { title = "Home" description = "Welcome" }
view {
<button @click="count++">Clicked {count} times</button>
}
}
`);
// ts is a TypeScript module: exports `meta`, and a default page component
// returning an HTML string, wrapped in a data-scope for the reactive runtime.
```
Inspect the AST and diagnostics:
```ts
import { compile, ParseError } from "@wrnexus/compiler";
try {
const { code, ast, diagnostics } = compile(source);
console.log(ast.kind, ast.name, ast.states.length);
} catch (err) {
if (err instanceof ParseError) console.error(err.message);
}
```
Drive the parse/codegen stages directly:
```ts
import { parse, generate } from "@wrnexus/compiler";
const ast = parse(componentSource); // ast.kind === "component"
const module = generate(ast); // exports render(props) + __wrnexusComponent
```
Use the lexer standalone:
```ts
import { Lexer } from "@wrnexus/compiler";
const lx = new Lexer("page Home {");
lx.next(); // { type: "ident", value: "page", pos: 0 }
lx.next(); // { type: "ident", value: "Home", pos: 5 }
lx.next(); // { type: "lbrace", value: "{", pos: 10 }
```
## The `.wrn` language (as parsed)
A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` body containing zero or more members:
- `layout = "<name>"` — selects `app/layouts/<name>.wrn` (pages only).
- `types { <TypeScript declarations> }` — reusable interfaces and aliases for the current file.
- `props { name: Type = <default> ... }` — typed component props. Omit `= <default>` to make a prop required. Legacy inferred props remain supported.
- `@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.
- `api <METHOD> <path> { <raw js> }` — route handler, lowered to a `METHOD` export (repeatable).
- `ssr { ... }` / `client { ... }` — data blocks holding `api <name> <METHOD> <path> { ... }` bindings and their own `functions { ... }`.
- `realtime <name> { on <evt>(<args>) { <raw js> } ... }` — websocket handlers, lowered to a `websocket` export.
`view` markup is parsed by a lenient dedicated HTML parser (`parseHtmlView`); HTML void elements (`<br>`, `<img>`, …) take no closing tag. Line comments (`//`) are skipped by the lexer.
## 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*`/`__wrn*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.
-49
View File
@@ -1,49 +0,0 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/compiler"
},
"homepage": "https://wrnexusjs.dev/packages/compiler",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"compiler"
],
"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/csr": "^0.8.16",
"@wrnexus/syntax": "^0.8.8",
"@wrnexus/store": "^0.8.8",
"@wrnexus/validation": "^0.8.9"
},
"files": [
"dist",
"README.md"
]
}
-412
View File
@@ -1,412 +0,0 @@
# @wrnexus/core
> The framework core: the request `Context`, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WrNexus package builds on.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/core` is the shared foundation of WrNexus. It defines the `Context`
object that flows through every middleware, page, and API route, plus the
`Middleware`/`Next` contract they implement. On top of that it ships the
building blocks a real app needs: cookie-backed sessions, password auth, CSRF
protection, rate limiting, request logging, HTTP + in-memory caching, file
uploads, streaming/SSE responses, WebSocket "rooms", security headers/CORS, and
a server-side JSX runtime that renders to HTML strings. Everything here is
**server-side** and Bun-native (it uses `Bun.password`, `Bun.write`, the
web-standard `Request`/`Response`, and `crypto`). You depend on it directly and
transitively through the rest of the framework.
## Installation
```bash
bun add @wrnexus/core
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### Context & middleware — `@wrnexus/core`
The `Context` (`ctx`) is the single value passed to middleware and handlers.
| Export | Kind | Description |
| ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------ |
| `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 (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`, … |
| `TFunction` | type | `(key, params?) => string` — translate a key for `ctx.lang`, interpolating `{param}` placeholders. |
Key `Context` fields:
- `ctx.locals` — per-request scratch space for passing values between middleware.
- `ctx.user` — the authenticated user (populated by `sessionAuth`/`logIn`), or `null`.
- `ctx.ip` — the direct socket peer IP (not spoofable via headers).
- `ctx.cookies` / `ctx.session` / `ctx.localStorage` — see **Storage** below.
### Authentication — `@wrnexus/core`
Passwords are hashed with argon2id via `Bun.password`; sessions ride the
cookie-backed `SessionStore`.
| Export | Signature | Notes |
| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `hashPassword(password)` | `(string) => Promise<string>` | argon2id hash to store. |
| `verifyPassword(password, hash)` | `(string, string) => Promise<boolean>` | Constant-safe; returns `false` on bad/empty hash. |
| `logIn(ctx, user)` | `(Context, U) => void` | Regenerates the session id (fixation defense), stores the user, sets `ctx.user`. |
| `logOut(ctx)` | `(Context) => void` | Clears the session and `ctx.user`. |
| `getUser(ctx)` | `(Context) => U \| null` | Current user from `ctx.user`, falling back to the session. |
| `sessionAuth()` | `() => Middleware` | Hydrates `ctx.user` from the session each request. Register early. |
| `requireAuth(options?)` | `(RequireAuthOptions?) => Middleware` | Guard: API/fetch requests get `401 JSON`, page navigations get `302` to `loginPath` (default `/login`) with `?next=`. |
| `SESSION_USER_KEY` | `"user"` | Session key holding the user. |
`RequireAuthOptions`: `{ loginPath?: string }`.
### CSRF — `@wrnexus/core`
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` | `"wrn-csrf"` / `"x-csrf-token"` | Cookie & header names. |
### Rate limiting — `@wrnexus/core`
Fixed-window limiter that returns `429` with `Retry-After` and emits
`RateLimit-Limit`/`-Remaining`/`-Reset` headers.
| Export | Signature | Notes |
| --------------------- | ----------------------------------- | ---------------------------------------------------------------------- |
| `rateLimit(options?)` | `(RateLimitOptions?) => Middleware` | Main middleware. |
| `peerKey(ctx)` | `(Context) => string` | Non-spoofable key from `ctx.ip` (default). |
| `proxyKey(ctx)` | `(Context) => string` | Trusts `x-forwarded-for`/`x-real-ip`. Use only behind a trusted proxy. |
| `defaultKey` | — | **Deprecated** alias of `proxyKey`. |
`RateLimitOptions`: `windowMs` (default `60_000`), `max` (default `60`),
`key`, `trustProxy` (default `false` → keys on `peerKey`; `true``proxyKey`),
`message`, `headers` (default `true`), `store`.
`RateLimitStore` is pluggable — implement `hit(key, windowMs, now) => Bucket | Promise<Bucket>`
(a `Bucket` is `{ count, resetAt }`) to back limits with Redis/SQL across
instances. The default store is process-local memory.
### Request logging — `@wrnexus/core`
| Export | Signature | Notes |
| ------------------------- | --------------------------------------- | -------------------------------------------------------------------------------- |
| `requestLogger(options?)` | `(RequestLoggerOptions?) => Middleware` | One record per request with a request id (stored on `ctx.locals[requestIdKey]`). |
`RequestLoggerOptions`: `format` (`"pretty"` default \| `"json"`), `sink(line, record)`
(default `console.log`), `requestIdKey` (default `"requestId"`), `now`.
`RequestRecord` = `{ time, id, method, path, status, durationMs }`.
### Resilience — `@wrnexus/core`
`resilientCall` standardizes cancellation-aware timeouts, controlled retries,
fixed or exponential backoff, fallback responses, circuit breaking, and bounded
concurrency. Reuse a declarative circuit/bulkhead options object, or an explicit
`CircuitBreaker`/`Bulkhead` instance, wherever calls must share health and
capacity state.
```ts
import { resilientCall } from "@wrnexus/core";
const paymentCircuit = { failures: 5, resetAfter: "30s" } as const;
const status = await resilientCall({
timeout: "5s",
retries: 3,
retryDelay: "100ms",
backoff: "exponential",
circuitBreaker: paymentCircuit,
bulkhead: { concurrency: 20, queue: 100 },
run: (signal) => paymentProvider.checkStatus({ signal }),
fallback: () => ({ state: "unavailable" }),
});
```
`CircuitBreaker.snapshot()` reports `closed`, `open`, or `half-open`, failure
and success counts, and the remaining retry delay for health endpoints and
development tooling. Fail-fast conditions use stable `WRN-RESILIENCE-*` codes.
Core's existing `HealthRegistry`, `withIdempotency`, and pluggable stores/locks
cover health reporting, idempotent requests, and distributed coordination.
### Caching — `@wrnexus/core`
| Export | Kind | Notes |
| -------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `TTLCache<V>` | class | In-memory TTL cache: `get`, `set`, `getOrLoad(key, loader, ttlMs?)`, `delete`, `clear`, `size`. Constructor takes a default `ttlMs` (60s). |
| `cacheControl(options)` | fn | Build a `Cache-Control` value from `CacheControlOptions`. |
| `withCacheControl(res, options)` | fn | Apply `Cache-Control` to a response. |
| `etag(body, weak?)` | fn | Stable quoted FNV-1a ETag (weak by default). |
| `notModified(req, tag)` | fn | `true` when `If-None-Match` matches — send a `304`. |
`CacheControlOptions`: `maxAge`, `sMaxAge`, `private`, `noStore`, `noCache`,
`staleWhileRevalidate`, `immutable`.
### File uploads — `@wrnexus/core`
Bun parses `multipart/form-data` via `Request.formData()`; these helpers
validate and persist the resulting `File`s.
| Export | Signature | Notes |
| --------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `collectUploads(form)` | `(FormData) => { field, file }[]` | Every non-empty `File` in a parsed form. |
| `saveUpload(file, options)` | `(File, SaveUploadOptions) => Promise<SavedUpload>` | Validates size/type, sanitizes the name, writes via `Bun.write`. Throws `UploadError`. |
| `sanitizeFilename(name)` | `(string) => string` | Strips separators, traversal, control/illegal chars; caps at 255. |
| `UploadError` | class | Thrown on rejected uploads. |
`SaveUploadOptions`: `dir` (required), `maxBytes`, `allowedTypes` (MIME types
like `"image/png"` and/or extensions like `".png"`), `filename(file)`.
`SavedUpload` = `{ path, filename, size, type }`.
### Streaming & SSE — `@wrnexus/core`
| Export | Signature | Notes |
| ------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `streamResponse(source, init?)` | `(Iterable\|AsyncIterable<string\|Uint8Array>, StreamResponseInit?) => Response` | Streaming `Response` from a chunk source (basis for streaming SSR). |
| `sse(source)` | `(Iterable\|AsyncIterable<ServerSentEvent>) => Response` | `text/event-stream` response. |
`StreamResponseInit`: `status`, `headers`, `contentType` (default
`"text/html; charset=utf-8"`). `ServerSentEvent`: `{ data, event?, id?, retry? }`.
### Realtime rooms — `@wrnexus/core`
WebSocket rooms. A file in `app/realtime/` exports
`default defineRoom({ ... })` and is served at `ws://host/realtime/<name>`.
| Export | Signature | Notes |
| --------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
| `defineRoom(handlers)` | `(RoomHandlers) => RoomDefinition` | Define a room. Export the result as `default`. |
| `isRoomDefinition(value)` | `(unknown) => boolean` | Type guard for a room definition. |
| `createRealtimeRegistry()` | `() => RealtimeRegistry` | Server-side connection manager mapping sockets ↔ rooms. |
| `bridgeRealtime(registry, bus, topic?)` | `(RealtimeRegistry, RealtimeBus, string?) => () => void` | Bridge broadcasts/`toUser` sends across processes via a pub/sub bus. |
`RoomHandlers`: `authorize(info) => boolean` (gate before accept — return
`false` to reject with 403), `onConnect(client)`, `onMessage(client, message)`
(JSON auto-parsed), `onLeave(client)`. A handler receives a `RoomClient` with
`id`, `user`, `query`, `data`, `room`, and `send` / `broadcast` /
`to(id)` / `toUser(user)` / `close`. The `Room` API adds `state`, `clients()`,
`count()`, and `broadcast`. `RealtimeBus` is structurally satisfied by
`@wrnexus/pubsub`. Legacy `RealtimeHandler`/`RealtimeSocket` raw handlers are
still exported. Connection-targeted sends (`send`, `to(id)`) stay local; room
broadcasts and `toUser` cross the bridge.
### Error pages — `@wrnexus/core`
| Export | Signature | Notes |
| ------------------------------ | -------------------------------- | ----------------------------------------------------- |
| `renderError(err, mode)` | `(unknown, Mode) => Response` | Dev page (with stack) or generic prod page by `mode`. |
| `renderDevError(err, status?)` | `(unknown, number?) => Response` | Readable HTML error page including the stack trace. |
| `renderProdError(status?)` | `(number?) => Response` | Generic page that never leaks file paths. |
| `renderNotFound()` | `() => Response` | Simple 404 page. |
`Mode` = `"development" | "production"`.
### Security headers & CORS — `@wrnexus/core`
| Export | Signature | Notes |
| -------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withSecurityHeaders(req, res, mode, security?, nonce?)` | → `Response` | Applies CORS + CSP, HSTS, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, COOP, Trusted Types, and `extraHeaders`. |
| `createCorsPreflightResponse(req, security?)` | → `Response \| null` | Builds a `204`/`403` preflight response for CORS `OPTIONS` requests. |
| `isWebSocketOriginAllowed(req, security?)` | → `boolean` | Guards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients). |
Config types: `SecurityConfig` (top-level), `CorsConfig`/`CorsOrigin`,
`ContentSecurityPolicyConfig`/`CspDirectiveValue`, `HstsConfig`,
`TrustedTypesConfig`, `PermissionsPolicyConfig`. WrNexus applies sensible
defaults (self-only CSP, `frame-ancestors 'none'`, restrictive Permissions-Policy,
HSTS in production, Trusted Types in production); each is individually
overridable or disable-able via `false`.
### Storage: cookies, sessions, localStorage — `@wrnexus/core`
These back the `ctx.cookies`, `ctx.session`, and `ctx.localStorage` fields.
| Export | Kind | Notes |
| --------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setSessionBackend(backend)` | fn | Swap the **sync** session persistence backend (`SessionBackend`) — e.g. `bun:sqlite`. Default is process-local memory. Call once at startup. |
| `loadSession(backend, options?)` | fn → `Middleware` | Back `ctx.session` with an **async** store (`AsyncSessionBackend`: `load`/`save`/`destroy`) — loads before the request, saves after. `options.ttlMs` default 24h. |
| `CookieStore` | type | `get`/`getAll`/`has`/`set(name, value, opts?)`/`delete`/`headers`. |
| `SessionStore` | type | `id`/`get`/`getAll`/`set`/`delete`/`regenerate`/`clear`. |
| `LocalStorageSnapshot` | type | Read-only view of the browser's localStorage sent via header for CSR bindings. |
| `CookieOptions` | type | `path`, `domain`, `maxAge`, `expires`, `httpOnly`, `secure`, `sameSite`. |
| `SessionEntry` / `SessionBackend` / `AsyncSessionBackend` | types | Session persistence contracts. |
### Low-level security helpers — `@wrnexus/core`
| Export | Signature | Notes |
| ----------------------------- | --------------------- | --------------------------------------------------- |
| `escapeHtml(value)` | `(string) => string` | Escape for HTML text/attributes. |
| `isSafeIslandName(name)` | `(string) => boolean` | Allow only a conservative `[A-Za-z0-9_-]+` charset. |
| `isSafeRequestPath(pathname)` | `(string) => boolean` | Reject NULs, `..` traversal, and backslashes. |
### JSX runtime — `@wrnexus/core`, `@wrnexus/core/jsx-runtime`, `@wrnexus/core/jsx-dev-runtime`
A server-side JSX runtime that renders to HTML **strings** (no virtual DOM).
Point `tsconfig`'s `jsxImportSource` at `@wrnexus/core`.
| Export | Kind | Notes |
| ------------------------------------------ | ------ | --------------------------------------------------------------------------------------- |
| `jsx` / `jsxs` | fn | The runtime factory (TypeScript calls these automatically). Returns an `Html` instance. |
| `Fragment` | symbol | JSX fragment marker. |
| `Html` | class | Wraps a raw, already-safe HTML string (`toString()` returns it). |
| `mustache(expr)` | fn | Emit a `{{expr}}` placeholder (tagged-template or string form) for the client binder. |
| `JSXComponent` / `JSXProps` / `Renderable` | types | Component signature and renderable value types. |
Values interpolated as children are HTML-escaped unless they are an `Html`
instance; use `dangerouslySetInnerHTML={{ __html }}` for trusted markup. Void
elements render without a closing tag; `className``class`, `htmlFor``for`, and
`style` objects are serialized to CSS text.
The subpath exports map to the runtime TypeScript's JSX transform expects:
```jsonc
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@wrnexus/core",
},
}
```
## Usage
### A minimal middleware chain
```ts
import {
createContext,
withContextHeaders,
sessionAuth,
requireAuth,
requestLogger,
rateLimit,
csrfProtection,
type Middleware,
} from "@wrnexus/core";
const chain: Middleware[] = [
requestLogger({ format: "json" }),
rateLimit({ max: 100, windowMs: 60_000 }),
csrfProtection(),
sessionAuth(),
requireAuth({ loginPath: "/login" }),
];
```
### Password auth
```ts
import { hashPassword, verifyPassword, logIn, getUser } from "@wrnexus/core";
// Registration
const passwordHash = await hashPassword(form.password);
// Login
if (await verifyPassword(form.password, user.passwordHash)) {
logIn(ctx, { id: user.id, email: user.email });
}
const current = getUser<{ id: string }>(ctx); // or null
```
### HTTP caching with ETags
```ts
import { etag, notModified, withCacheControl } from "@wrnexus/core";
const body = JSON.stringify(data);
const tag = etag(body);
if (notModified(ctx.req, tag)) {
return new Response(null, { status: 304, headers: { ETag: tag } });
}
const res = new Response(body, { headers: { ETag: tag, "content-type": "application/json" } });
return withCacheControl(res, { maxAge: 60, staleWhileRevalidate: 300 });
```
### Streaming SSE
```ts
import { sse } from "@wrnexus/core";
async function* ticks() {
for (let n = 0; ; n++) {
yield { event: "tick", data: String(n) };
await Bun.sleep(1000);
}
}
export default (ctx) => sse(ticks());
```
### A realtime room
```ts
// app/realtime/chat.ts
import { defineRoom } from "@wrnexus/core";
export default defineRoom({
authorize: (info) => !!info.user, // require auth
onConnect(client) {
client.user = client.query.user;
client.room.broadcast({ type: "join", id: client.id });
},
onMessage(client, msg) {
client.broadcast({ type: "say", from: client.id, text: msg.text });
},
});
```
Scale it across processes:
```ts
import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
const registry = createRealtimeRegistry();
bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL)));
```
### JSX rendering
```tsx
import { Html } from "@wrnexus/core";
function Card({ title, body }: { title: string; body: string }) {
return (
<article class="card">
<h2>{title}</h2>
<p>{body}</p>
</article>
);
}
const html: Html = <Card title="Hi" body="<b>escaped</b> automatically" />;
return new Response(html.toString(), { headers: { "content-type": "text/html" } });
```
## Requirements / Notes
- **Bun-only.** Uses `Bun.password` (argon2id), `Bun.write`, web-standard
`Request`/`Response`/`FormData`/`ReadableStream`, and the global `crypto`.
Node is not supported.
- Session and rate-limit backends default to **process-local memory**. For
multi-instance deployments, swap in a shared backend: `setSessionBackend` (sync,
e.g. `bun:sqlite`) or `loadSession` (async, e.g. Redis) for sessions, a custom
`RateLimitStore` for limits, and `bridgeRealtime` for realtime.
- Works with the rest of the framework: realtime bridging is structurally
compatible with [`@wrnexus/pubsub`](../pubsub); the security, auth, and JSX
primitives here are consumed by the WrNexus server/router packages.
- Subpath exports: `@wrnexus/core/jsx-runtime` and `@wrnexus/core/jsx-dev-runtime`
for TypeScript's automatic JSX transform.
-51
View File
@@ -1,51 +0,0 @@
{
"name": "@wrnexus/core",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/core"
},
"homepage": "https://wrnexusjs.dev/packages/core",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"core"
],
"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"
},
"./jsx-runtime": {
"types": "./dist/jsx-runtime.d.ts",
"import": "./dist/jsx-runtime.js"
},
"./jsx-dev-runtime": {
"types": "./dist/jsx-dev-runtime.d.ts",
"import": "./dist/jsx-dev-runtime.js"
}
},
"files": [
"dist",
"README.md"
]
}
-224
View File
@@ -1,224 +0,0 @@
# @wrnexus/csr
## Navigation state preservation
Pages can opt into restoration across client navigation:
```wrn
page Users {
navigation {
preserve = ["filters", "pagination", "scroll", "tabs", "expanded"]
}
}
```
Form-like categories restore named inputs, selects, and textareas. Password,
file, hidden, CSRF/token/secret/credential fields, and elements marked
`data-no-preserve` are never saved. For tab, expanded, or component UI state,
mark stable elements with `data-wrn-preserve="key"`; their value and ARIA
selected/expanded state are restored. State is scoped to pathname plus query.
## Typed server actions
`createActionClient<Input, Output>(route, name)` supports programmatic calls.
Schema-backed WRN actions also export `__wrnexusActionClients`, whose input and
output are inferred automatically. Enhanced forms expose
`data-wrn-action-state="pending|success|error"` and dispatch bubbling
`wrnexus:action:optimistic`, `:pending`, `:success`, and `:error` events.
Success details contain returned data and invalidated cache tags; error details
contain field errors. Without JavaScript, the same form posts to its page and
receives a 303 redirect or accessible validation response.
> The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/csr` holds the three client runtimes that WrNexus serves to the browser. Components are authored as `.wrn` files and rendered on the **server**; this package provides the single, generic runtime that **hydrates** that HTML in the browser — there are no per-component browser bundles. Each runtime is exported as a plain-JS string (no build step, no imports) intended to be served verbatim from a well-known URL:
- **reactive** at `/__wrnexus/reactive.js` — reactive directives (`data-scope`, `data-text`, `data-for`, …)
- **nav** at `/__wrnexus/nav.js` — SPA-style client navigation with graceful fallback
- **realtime** at `/__wrnexus/realtime.js` — WebSocket "rooms", declarative or programmatic
The package itself runs on the server (it just returns strings); the strings it returns run in the browser. A dev/prod server (see `@wrnexus/core`) is responsible for actually serving them.
## Installation
```bash
bun add @wrnexus/csr
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
All exports come from the package root (`@wrnexus/csr`). The runtime source is delivered as strings, so the "API" on the server side is small; the real surface is the browser directives/globals each string installs.
### Runtime strings
| Export | Type | Served at | Contents |
| ------------------ | -------- | ------------------------ | ------------------------------ |
| `REACTIVE_RUNTIME` | `string` | `/__wrnexus/reactive.js` | Reactive directive runtime |
| `NAV_RUNTIME` | `string` | `/__wrnexus/nav.js` | Client-side navigation runtime |
| `REALTIME_RUNTIME` | `string` | `/__wrnexus/realtime.js` | Realtime rooms runtime |
### Accessor functions
Convenience getters that return the same strings.
```ts
getReactiveRuntime(): string // → REACTIVE_RUNTIME
getNavRuntime(): string // → NAV_RUNTIME
getRealtimeRuntime(): string // → REALTIME_RUNTIME
```
### Browser: reactive directives
Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works.
| Directive | Purpose |
| ---------------------------------- | ---------------------------------------------------- |
| `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree |
| `data-on-<event>="count++"` | Run a statement in scope on a DOM event |
| `data-text="expr"` | Bind an element's `textContent` to an expression |
| `data-show="expr"` | Toggle visibility while preserving interactive state |
Compiled conditional rendering and dynamic component cases omit inactive elements from the live
DOM. `data-show` is a visibility directive for stateful controls and keeps its element mounted.
Neither mechanism is authorization: never place secrets in client-rendered branches. Authorize on
the server and return only data the current request may access.
| `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder |
| `data-key="item.id"` | Alternative key declaration for `data-for` templates |
| `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values |
| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) |
Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it.
Browser globals installed: `window.__wrnexusHydrateScopes(root)` and `window.__wrnexusHydrateCsrFetches(root)` — both idempotent, so re-running after a DOM swap or HMR morph is safe. Both run automatically on `DOMContentLoaded`.
### Browser: navigation
Intercepts same-origin `<a>` clicks, fetches the target page, and swaps the `#app` container in place (via `importNode` — not `innerHTML` — so it works under a Trusted-Types CSP), updating history, title, and scroll, then re-hydrates. Cross-origin links, modified clicks, `download`/`data-no-nav`/`rel="external"`/`target` links, non-HTML responses, or a missing `#app` fall back to a full browser navigation.
- Programmatic navigation: `window.__wrnexusNavigate(url)`
- Emits a `wrnexus:navigated` `CustomEvent` (`detail.url`) after each swap
- Sends `x-wrnexus-nav: 1` on fetches so the server can return the page fragment
- Appends any `/__wrnexus/*` runtime scripts the incoming page needs but the current document lacks
### Browser: realtime rooms
Connects to `/realtime/<name>` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes.
Programmatic API via `window.wrn`:
```ts
wrn.room(name): Room // open (or reuse) a room connection
wrn.bindRooms(root?) // (re)bind declarative [data-room] containers
interface Room {
name: string;
send(obj: object | string): Room; // JSON-stringifies objects; queues until open
on(type: string, cb): Room; // filter by msg.type; "*" or a fn = all messages
on(cb): Room;
close(): Room;
}
```
Internal lifecycle messages are emitted to listeners as `{ type }`: `__open`, `__close`, `__error`, and `__raw` (non-JSON frames, with `data`). Reconnect uses exponential backoff capped at 5s; queued sends flush on reconnect.
Declarative binding (zero JS) on a `data-room="<name>"` container:
| Attribute | On | Purpose |
| ------------------------------------ | --------------- | ------------------------------------------------------------------------ |
| `data-room="<name>"` | container | Connect to room `<name>` |
| `data-room-user="<id>"` | container | Identify the connection (`?user=<id>`) |
| `data-room-log` | element | Where incoming messages are appended |
| `<template data-room-item="<type>">` | template | Row template for messages of that `type` (empty = fallback) |
| `%field%` | inside template | Placeholder filled from the message field (text/attr only, HTML-escaped) |
| `data-room-status` | element | Reflects connection state text (`connected`/`disconnected`/`error`) |
| `data-room-status-class` | status element | Base class; a state variant (`is-connected`, …) is appended |
| `<form data-room-send>` | form | Submits named fields as a JSON message |
| `data-room-reset` | form field | Clears that field after send |
Rebinds on `wrnexus:navigated` and closes rooms whose container has left the page.
## Usage
Server side — serve the runtime strings from your router (example with `Bun.serve`):
```ts
import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";
const routes: Record<string, string> = {
"/__wrnexus/reactive.js": getReactiveRuntime(),
"/__wrnexus/nav.js": getNavRuntime(),
"/__wrnexus/realtime.js": getRealtimeRuntime(),
};
Bun.serve({
fetch(req) {
const body = routes[new URL(req.url).pathname];
if (body) {
return new Response(body, {
headers: { "content-type": "text/javascript; charset=utf-8" },
});
}
return new Response("Not found", { status: 404 });
},
});
```
Browser side — server-rendered HTML that the reactive runtime hydrates:
```html
<div data-scope="count: 0, showPassword: false">
<button data-on-click="count++">+1</button>
<span data-text="count"></span>
<p>Total: {{count}}</p>
<input type="{showPassword ? 'text' : 'password'}" />
<button
data-on-click="showPassword = !showPassword"
aria-label="{showPassword ? 'Hide password' : 'Show password'}"
>
Toggle password
</button>
</div>
<script src="/__wrnexus/reactive.js"></script>
```
State interpolation in ordinary attributes is reactive. The compiler keeps the
initial SSR value and emits an internal binding so attributes such as `type`,
`aria-label`, `aria-pressed`, `class`, and `href` update after state changes.
A realtime chat, fully declarative:
```html
<div data-room="lobby" data-room-user="ada">
<div data-room-status></div>
<ul data-room-log></ul>
<template data-room-item="chat"><li>%user%: %text%</li></template>
<form data-room-send>
<input name="text" data-room-reset />
<input type="hidden" name="type" value="chat" />
<button>Send</button>
</form>
</div>
<script src="/__wrnexus/realtime.js"></script>
```
Or drive a room from code:
```ts
const room = wrn.room("lobby");
room.on("chat", (msg) => console.log(msg.user, msg.text));
room.send({ type: "chat", user: "ada", text: "hi" });
```
## Requirements / Notes
- **Bun-only** on the server (the package integrates with Bun-based WrNexus servers); the emitted strings are plain browser JS with no dependencies.
- Browser runtimes are **self-contained** (no imports, no build step) and **idempotent**, so re-hydration after navigation or HMR is safe.
- Designed for a **strict CSP**: the reactive expression evaluator avoids `eval`/`new Function` (no `unsafe-eval`), and DOM swaps use `importNode`/attribute writes rather than `innerHTML` (Trusted-Types friendly).
- Peer packages: rendered `.wrn` components and the serving layer come from `@wrnexus/core` (the sole dependency); pages are rendered by the WrNexus dev/prod server.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/csr",
"version": "0.8.16",
"type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/csr"
},
"homepage": "https://wrnexusjs.dev/packages/csr",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"csr"
],
"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.9"
},
"files": [
"dist",
"README.md"
]
}
-299
View File
@@ -1,299 +0,0 @@
# @wrnexus/db
## Rollout-safe migrations
Run `wrnexus db check` in CI before deployment. The analyzer reports stable
diagnostics for drops, renames, type changes, new/enforced required columns,
and potentially blocking index creation, with an expand/backfill/switch/contract
recommendation. `wrnexus db migrate` blocks critical issues in pending
migrations. `--allow-breaking` is an explicit operator override; already-applied
migrations do not block later releases.
> The database layer for WrNexus: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based `Db` client, migrations, and a sqlc-style query generator.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/db` is the server-side data layer. You describe tables as TypeScript
models (the `v` column builder + `table()`); those models drive migrations,
coerce raw DB rows into typed objects, and feed the query generator. A thin
`Driver` interface is implemented by adapters for SQLite (`bun:sqlite`),
Postgres/MySQL (`Bun.SQL`), and MongoDB. The `Db` client adds ergonomics —
model-mapped `all`/`one`, transactions, `createTable`, pagination, and batched
relation loading. A process-wide registry (`getDb`/`setDb`) exposes configured
connections to pages and API routes. Reach for it whenever a WrNexus app needs
persistence.
## Installation
```bash
bun add @wrnexus/db
```
> 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 core entry (`@wrnexus/db`) is dependency-free; adapters and connectors live
in subpaths so importing the core doesn't pull in every driver.
| Subpath | Exports |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@wrnexus/db` | `v`, `table`, `Column`, `createDb`, `createTableSql`, the client registry (`setDb`/`getDb`/…), migrations, the query generator, and query helpers |
| `@wrnexus/db/connect` | `connectFromConfig`, `resolveDbUrl`, `DbConfig` — resolve a config to a live SQL `Db` |
| `@wrnexus/db/session` | `sqliteSessionStore` — a `bun:sqlite` session backend for `@wrnexus/core` |
| `@wrnexus/db/sqlite` | `sqlite(url?)` driver |
| `@wrnexus/db/postgres` | `postgres(url)` driver |
| `@wrnexus/db/mysql` | `mysql(url)` driver |
| `@wrnexus/db/mongo` | `mongo(url, dbName?)` document API |
### Schema — `v`, `table`, `Column`
`table(name, columns)` returns a `Model<T>`. Columns are built with `v`:
```ts
import { v, table } from "@wrnexus/db";
const users = table("users", {
id: v.id(), // auto-increment primary key
email: v.text().unique(),
name: v.text().optional(), // NULLable
age: v.int().default(0),
active: v.bool().default(true),
createdAt: v.timestamp().default("now"), // CURRENT_TIMESTAMP
});
```
Column builders: `v.id`, `v.text` (alias `v.string`), `v.int`, `v.real` (alias
`v.number`), `v.bool` (alias `v.boolean`), `v.timestamp`, `v.json`. `BaseType`
values are `"id" | "text" | "int" | "real" | "bool" | "timestamp" | "json"`.
`Column` modifiers (chainable): `.optional()`, `.unique()`, `.default(value)`
(use the sentinel `"now"` for a current-timestamp default), `.primaryKey()`,
`.references(table, column = "id")`. `.coerce(raw)` converts a raw DB value to
its JS type.
A `Model<T>` exposes: `name`, `columns`, `parse(row)` (coerces a raw row into a
typed `T`; unknown columns pass through), and `describe()` (returns each
column's `ColumnDef`, for migrations and the generator).
### Driver & client — `createDb`, `Db`, `Driver`
```ts
createDb(driver: Driver): Db
```
A `Driver` (implemented by adapters) exposes `dialect`, `query(sql, params?)`,
`exec(sql, params?)`, `transaction(fn)`, and `close()`. `createDb` wraps it in a
`Db`:
- `all<T>(sql, params?, model?)` — all rows, mapped through `model.parse` when a model is given.
- `one<T>(sql, params?, model?)` — first row or `null`.
- `exec(sql, params?)``Promise<ExecResult>` (`{ changes, lastInsertId? }`).
- `tx(fn)` — run `fn(db)` in a transaction; rolls back on throw. Nested `tx` reuses the current transaction.
- `createTable(model)` — runs the model's `CREATE TABLE IF NOT EXISTS` DDL.
- `close()` — idempotently rejects new top-level work, drains active queries and
transactions, then closes the underlying pool.
Every query is parameterized (positional params). `createTableSql(model, dialect, ifNotExists?)`
renders `CREATE TABLE` directly; `Dialect` is `"sqlite" | "postgres" | "mysql"`.
### Client registry — `getDb` / `setDb`
A process-wide registry the runtime configures at startup from `wrnexus.config.ts`
(the `db` setting is the default; `databases.<name>` entries are named).
- `setDb(db)` / `setDb(name, db)` — set the default or a named connection.
- `registerDb(name, db)` — alias of `setDb(name, db)`.
- `getDb(name = "default")` — the default or a named `Db` (throws if unconfigured).
- `hasDb(name?)`, `databaseNames()`, `closeDatabases()`. Registry shutdown clears
registrations first, attempts every open database, and reports close failures
together with `AggregateError` instead of leaking later pools.
```ts
const users = await getDb().all("SELECT * FROM users");
const events = await getDb("analytics").all("SELECT * FROM hits");
```
### Adapters
- `@wrnexus/db/sqlite``sqlite(url = ":memory:")`. `url` may be `file:./dev.db`, a raw path, or `:memory:`. Built on `bun:sqlite`; no external service.
- `@wrnexus/db/postgres``postgres(url)` (e.g. `postgres://user:pass@host:5432/db`, placeholders `$N`).
- `@wrnexus/db/mysql``mysql(url)` (e.g. `mysql://user:pass@host:3306/db`, placeholders `?`). Postgres/MySQL both use Bun's native `Bun.SQL` client and its pooled `begin()` for transactions.
- `@wrnexus/db/mongo``mongo(url, dbName?)`. A document API, not SQL: `db.collection(model)` returns a `MongoRepo<T>` with `find`, `findOne`, `insert`, `insertMany`, `update`, `delete`, `count`. Reads are coerced through `model.parse` (`_id` is mapped to `id`). The `mongodb` driver is imported lazily — install it to use Mongo.
### Migrations
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 `_wrn_migrations` table so each runs once.
- `parseMigration(name, content)``Migration` (`{ name, up, down }`).
- `loadMigrations(dir)` — parse all `.sql` files, sorted by filename.
- `appliedMigrations(db)` — applied names, oldest first.
- `migrate(db, dir, options?)` — apply all pending (each in a transaction); returns applied names.
- `rollback(db, dir, options?)` — roll back the most recent; returns its name or `null`.
- `status(db, dir)``{ name, applied }[]` for every migration file.
- `scaffoldMigration(dir, name, dialect, models?)` — write a new numbered migration; with `models` it generates `CREATE`/`DROP` for every table (referenced tables first via topological sort). Returns the file path.
### Query generator (sqlc-style)
Turns annotated SQL into typed TS functions; params and result types are
inferred from the models, and rows map back through `model.parse` when the
selected columns are model columns.
- `parseQueries(content)``QueryDef[]` from `-- name: X :one|:many|:exec` blocks.
- `generateQueriesFile(queries, models, dialect)` → the `queries.gen.ts` source. `models` is a `ModelRef[]` (`{ varName, model }`). Rewrites `:name` placeholders to positional (`$N`/`?`) form.
`QueryKind` is `"one" | "many" | "exec"`.
### Query helpers
- `paginate(db, { sql, params?, countSql?, model? }, opts?)` — offset pagination. Pass the base SELECT **without** a LIMIT; it appends the page window and derives `total` via a COUNT subquery. `PageOptions`: `{ page?, perPage?, maxPerPage? }` (defaults page 1, perPage 20, maxPerPage 100). Returns `Paginated<T>` (`items, page, perPage, total, totalPages, hasNext, hasPrev`).
- `loadRelated(db, parents, opts)` — load a relation for many parents in ONE query and attach it (no N+1). `RelationOptions`: `{ table, foreignKey, as, localKey?, single?, model? }``single: true` attaches one child (belongsTo), otherwise an array (hasMany). Table/foreign-key names are validated as identifiers.
### Session store
`@wrnexus/db/session` exports `sqliteSessionStore(path = "sessions.db")`, a
persistent, process-shared `SessionBackend` (from `@wrnexus/core`) backed by
`bun:sqlite` (WAL mode). Sessions survive restarts and are shared by every
worker on the same file.
## Usage
Define models, connect, create tables, and query with typed results:
```ts
import { v, table, createDb } from "@wrnexus/db";
import { sqlite } from "@wrnexus/db/sqlite";
const users = table<{ id: number; email: string; name: string | null }>("users", {
id: v.id(),
email: v.text().unique(),
name: v.text().optional(),
createdAt: v.timestamp().default("now"),
});
const db = createDb(sqlite("file:./dev.db"));
await db.createTable(users);
await db.exec("INSERT INTO users (email) VALUES (?)", ["a@b.com"]);
const list = await db.all("SELECT * FROM users", [], users); // rows typed + coerced
const one = await db.one("SELECT * FROM users WHERE id = ?", [1], users);
await db.tx(async (tx) => {
await tx.exec("UPDATE users SET name = ? WHERE id = ?", ["Ada", 1]);
});
```
Resolve a config to a live SQL `Db`, and register it:
```ts
import { connectFromConfig } from "@wrnexus/db/connect";
import { setDb, getDb } from "@wrnexus/db";
setDb(connectFromConfig({ driver: "sqlite", url: "file:./dev.db" }, process.cwd()));
const rows = await getDb().all("SELECT * FROM users");
```
Run migrations and paginate:
```ts
import { migrate, paginate } from "@wrnexus/db";
await migrate(db, "app/db/migrations");
const pageTwo = await paginate(
db,
{ sql: "SELECT * FROM users ORDER BY id", model: users },
{ page: 2 },
);
```
For deployments, `{ dryRun: true }` reports pending names without applying
their SQL, `signal` cancels safely between migrations, and the default
database-backed lock prevents concurrent deploy runners. A live lock produces
`WRN-DB-MIGRATION-LOCKED`; crash-stale locks expire after `lockTimeoutMs` (five
minutes by default). Disable it with `lock: false` only when an external deploy
coordinator already guarantees exclusivity.
```ts
const pending = await migrate(db, "app/db/migrations", { dryRun: true });
await migrate(db, "app/db/migrations", {
signal: shutdownController.signal,
lockTimeoutMs: 10 * 60_000,
});
```
MongoDB (document API):
```ts
import { mongo } from "@wrnexus/db/mongo";
const mdb = await mongo(process.env.MONGO_URL!, "app");
const repo = mdb.collection(users);
await repo.insert({ email: "a@b.com" });
const active = await repo.find({ active: true });
```
## Configuration
`connectFromConfig` (and the runtime) read a `DbConfig` (`{ driver, url }`)
where `driver` is `sqlite | postgres | mysql`. `resolveDbUrl(url, appRoot?)`
resolves a relative `file:`/`sqlite:` URL against the app root. MongoDB is not a
SQL driver — use `@wrnexus/db/mongo` directly.
## Requirements / Notes
- **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 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.
## Repository and transaction helpers
Repositories accept an immutable equality `scope`, normally `{ column: "tenant_id", value:
ctx.tenant.id }`. The scope is injected into every read, count, update and delete, while create
overwrites any caller-supplied tenant value. This makes accidental cross-tenant CRUD through the
repository API fail closed.
```ts
import { createRepository, retryTransaction, databaseHealth, batch } from "@wrnexus/db";
const users = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
});
const user = await users.require(42);
await users.update(42, { active: true });
```
Repository identifiers are validated, writes may be restricted to an allowlist, and values always use query parameters. Infrastructure packages remain helper-only and do not add UI dependencies to server code.
## 0.8 repository and transaction helpers
```ts
import { createRepository, databaseHealth, firstOrThrow, retryTransaction } from "@wrnexus/db";
const usersRepo = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
maxListLimit: 250,
});
const users = await usersRepo.all({
orderBy: "name",
direction: "asc",
limit: 50,
offset: 0,
});
```
Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses `$1`, `$2`, and SQLite/MySQL use `?`. List limits are bounded.
`retryTransaction()` retries recognized serialization, deadlock, and database-lock errors by default. Supply `shouldRetry` for application-specific retryable errors; ordinary validation or business errors are not retried automatically.
-67
View File
@@ -1,67 +0,0 @@
{
"name": "@wrnexus/db",
"version": "0.8.9",
"type": "module",
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/db"
},
"homepage": "https://wrnexusjs.dev/packages/db",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"db"
],
"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"
},
"./connect": {
"types": "./dist/connect.d.ts",
"import": "./dist/connect.js"
},
"./session": {
"types": "./dist/session-store.d.ts",
"import": "./dist/session-store.js"
},
"./sqlite": {
"types": "./dist/adapters/sqlite.d.ts",
"import": "./dist/adapters/sqlite.js"
},
"./postgres": {
"types": "./dist/adapters/postgres.d.ts",
"import": "./dist/adapters/postgres.js"
},
"./mysql": {
"types": "./dist/adapters/mysql.d.ts",
"import": "./dist/adapters/mysql.js"
},
"./mongo": {
"types": "./dist/adapters/mongo.d.ts",
"import": "./dist/adapters/mongo.js"
}
},
"files": [
"dist",
"README.md"
]
}
-311
View File
@@ -1,311 +0,0 @@
# @wrnexus/dev-server
> The WrNexus HTTP + WebSocket server runtime — request dispatch, SSR document assembly, live-reload (HMR), and the portable production handler.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## 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 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
```bash
bun add @wrnexus/dev-server
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported for the full server; the `node:http` adapter is for WinterCG embedding only).
## API
### 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, 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`.
### `startServer(opts)`
```ts
interface ServeOptions {
appDir: string; // absolute/relative path to the app/ dir
port?: number; // default 3000
hostname?: string; // default "localhost"
mode?: Mode; // "development" | "production"; default "development"
hmr?: boolean; // inject live-reload client; default (mode === "development")
styleEntry?: string | null; // resolved absolute path to the global CSS entry
stylesConfig?: StylesConfig; // custom styles processor (e.g. Tailwind/PostCSS)
head?: string; // raw HTML appended to every page <head>
seo?: SeoConfig; // global SEO defaults
security?: SecurityConfig; // security headers + CORS policy
theme?: ThemeConfig; // design-token theme (merged over built-in light/dark)
i18n?: I18nConfig; // default language + supported locales
db?: { driver: string; url: string }; // default db → getDb(); dev auto-migrates
databases?: Record<string, { driver: string; url: string }>; // named dbs → getDb("<name>")
realtime?: { scale?: boolean; redisUrl?: string }; // bridge rooms over Redis across processes
}
interface RunningServer {
port: number;
hostname: string;
url: string;
router: Router;
stop(): void;
}
```
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.
`getWrnCompileMetrics()` exposes cumulative content-addressed compiler cache
`hits`, `misses`, successful `compilations`, `errors`, `totalDurationMs`, and
`lastDurationMs` for the DevToolbar or custom diagnostics. Tests and embedded
servers can call `resetWrnCompileMetrics()` to establish a fresh measurement
window.
### `createHandlers(deps)`
The core runtime shared by dev and prod. It handles CORS preflight, `/healthz` and `/__wrnexus/health`, request-body size limits (413), HMR socket upgrades (`/__wrnexus/hmr`), realtime WebSocket upgrades (`defineRoom` default export or a raw `websocket` export), the middleware pipeline, API routes (`/api/*`), framework assets (`/__wrnexus/*`), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).
```ts
interface RuntimeDeps {
mode: Mode;
hmr: boolean; // inject the live-reload client into pages
router: Router;
loadModule(file: string): Promise<Record<string, unknown>>;
getMiddleware(): Promise<Middleware[]>;
assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
hasStyles?: boolean; // inject the global stylesheet link
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>
assetVersion?: string; // cache-busting ?v= on framework asset URLs
head?: string; // raw HTML appended to every page <head>
seo?: SeoConfig;
security?: SecurityConfig;
maxBodyBytes?: number; // 413 above this; default 10 MB
hub?: HmrHub; // browser HMR sockets (dev only)
realtimeBus?: RealtimeBus; // cross-process room bridge (Redis pub/sub)
}
interface Handlers {
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
websocket: { open; message; close; drain };
}
```
`WsData` is the per-connection socket tag — a discriminated union of `{ kind: "realtime"; handler }`, `{ kind: "room"; meta }`, or `{ kind: "hmr" }`.
### `createProductionServer(manifest, opts)` / `createProductionHandlers(manifest, opts)`
Production runs the _same_ request runtime as dev, but with no filesystem scan and no runtime bundling. `wrnexus build` emits an entry that statically imports every route/component/layout module and passes them as a `ProdManifest`; the route-matching tables are rebuilt from the raw patterns.
```ts
interface ProdManifest {
pages: { raw: string; mod: RouteModule }[];
api: { raw: string; mod: RouteModule }[];
realtime: { raw: string; mod: RouteModule }[];
middleware: Middleware[];
components: { name: string; mod: RouteModule }[];
layouts: { name: string; mod: RouteModule }[];
}
interface ProdOptions {
stylesPath?: string;
inlineStyles?: string;
reactivePath?: string;
themePath?: string;
themeJsPath?: string;
theme?: ResolvedTheme;
uiCssPath?: string;
schemasJs?: string;
i18n?: ResolvedI18n;
db?: { driver: string; url: string };
databases?: Record<string, { driver: string; url: string }>;
realtime?: { scale?: boolean; redisUrl?: string };
assetVersion?: string;
publicDir?: string;
head?: string;
seo?: SeoConfig;
security?: SecurityConfig;
port?: number;
hostname?: string;
maxBodyBytes?: number;
}
```
`createProductionServer` also loads the `.env` cascade for the `production` profile, installs `SIGTERM`/`SIGINT` graceful shutdown, and binds `0.0.0.0` (port from `opts.port` or `$PORT`, default 3000). Migrations are **not** run here — apply them first (`wrnexus db migrate`). `createProductionHandlers` returns the bare handlers for edge/serverless/`node:http` deployment.
### `startGateway(opts)` — multi-app gateway
Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, normal application edits are applied inside the existing child and sent through its existing HMR connection. The child supervisor remains as crash recovery rather than the normal update path. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
```ts
interface GatewayOptions {
port?: number; // default 3000
hostname?: string; // dev: "127.0.0.1"; production: "0.0.0.0"
mode?: "development" | "production";
apps: GatewayApp[];
security?: GatewaySecurity;
}
interface GatewayApp {
name: string; // app id (for logs)
dir: string; // app root (contains app/ + wrnexus.config.ts)
domains: string[]; // host names routed here
port?: number; // fixed internal port; else assigned
auth?: GatewayAuth; // per-app edge access control
}
interface GatewayAuth {
basic?: { user: string; pass: string } | Array<{ user: string; pass: string }>;
allowIps?: string[]; // exact-match IP allowlist
forward?: { url: string }; // forward-auth (SSO): 2xx allows
}
interface GatewaySecurity {
trustedHostsOnly?: boolean; // 404 unknown hosts instead of first app
rateLimit?: { max: number; windowMs?: number }; // global by client IP (429)
headers?: boolean; // add baseline edge security headers
forwardedHeaders?: boolean; // set X-Forwarded-* (default true)
accessLog?: boolean;
}
```
Forward auth is a verification hook, not a login page. Configure `forward.url` with a
dedicated endpoint such as `http://sso.localhost:3000/api/verify`. The gateway forwards
the request's `Cookie` and `Authorization` headers plus `X-Forwarded-Host`,
`X-Forwarded-Proto`, `X-Original-Method`, and `X-Original-Uri` (including its query
string). The verifier must return 2xx only for an authenticated session and 401/403
otherwise. Pointing forward auth at an SSO home page that always returns 200 allows
every request and does not implement SSO.
For browser SSO, the verifier may return a `302`/`303`/`307`/`308` with a `Location`
header pointing to its login page. The gateway passes that redirect to the browser. The
login flow should validate a signed `returnTo` value before redirecting back; API clients
should receive `401`/`403` instead of an HTML login redirect.
Open the gateway URL (normally `http://127.0.0.1:3000`), not an app's internal
port. The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a
`RunningGateway` (`{ port, url, stop() }`). Use `--host=0.0.0.0` when other devices need
to reach a development gateway.
### `node:http` adapter (from `./adapters/node.ts`)
For embedding the WinterCG handler behind an existing Node server or a WinterCG host. Note the full app still needs Bun-compatible globals (`Bun.file`, `bun:sqlite`, etc.); only the `Request`/`Response` conversion is fully portable.
```ts
type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;
toRequest(req: IncomingMessage, opts?): Promise<Request>
writeResponse(res: ServerResponse, response: Response): Promise<void> // preserves multiple Set-Cookie
nodeListener(handler: FetchHandler, opts?): (req, res) => Promise<void>
serveNode(handler: FetchHandler, opts?): Promise<Server>
```
### Subpath export: `@wrnexus/dev-server/serve-entry`
The child process the dev supervisor launches:
```bash
bun run serve-entry.ts <appDir> <port> <mode>
```
It loads the optional `wrnexus.config.ts`, resolves the style entry, calls `startServer`, and prints the route table (Pages / API / Realtime / Components). Because it runs in its own process, every restart re-imports all route modules fresh — that is how the supervisor delivers live reload of edited server code. `startGateway` resolves this entry via `import.meta.resolve("@wrnexus/dev-server/serve-entry")` to spawn each dev app.
## Usage
### Programmatic dev server
```ts
import { startServer } from "@wrnexus/dev-server";
const server = await startServer({
appDir: "./app",
port: 3000,
mode: "development",
theme: {/* design tokens */},
db: { driver: "sqlite", url: "file:./data/app.db" },
});
console.log(`Running at ${server.url}`);
// server.stop();
```
### Production server from a build manifest
```ts
import { createProductionServer } from "@wrnexus/dev-server";
import { manifest } from "./dist/manifest.js"; // generated by `wrnexus build`
createProductionServer(manifest, {
stylesPath: "./dist/styles.css",
reactivePath: "./dist/reactive.js",
assetVersion: process.env.BUILD_ID,
db: { driver: "postgres", url: process.env.DATABASE_URL! },
port: Number(process.env.PORT) || 3000,
});
```
### Embedding the handler on `node:http`
```ts
import { createProductionHandlers, serveNode } from "@wrnexus/dev-server";
const handlers = createProductionHandlers(manifest, opts);
await serveNode(handlers.fetch, { port: 8080 });
```
### Multi-app gateway
```ts
import { startGateway } from "@wrnexus/dev-server";
await startGateway({
port: 3000,
apps: [
{ name: "web", dir: "./apps/web", domains: ["localhost", "web.localhost"] },
{
name: "admin",
dir: "./apps/admin",
domains: ["admin.localhost"],
auth: { basic: { user: "root", pass: "s3cret" } },
},
],
security: { trustedHostsOnly: true, rateLimit: { max: 600 } },
});
```
## Framework asset routes
The runtime serves these framework-owned paths (dev builds them live; prod serves pre-built/immutable versions):
- `/__wrnexus/nav.js`, `/__wrnexus/reactive.js`, `/__wrnexus/realtime.js` — client runtimes
- `/__wrnexus/validate.js`, `/__wrnexus/schemas.js`, `/__wrnexus/i18n.js` — validation + i18n runtimes
- `/__wrnexus/theme.css`, `/__wrnexus/theme.js`, `/__wrnexus/ui.css`, `/__wrnexus/styles.css` — styles
- `/__wrnexus/hmr` — dev-only HMR WebSocket
- `/__wrnexus/csr` — server-evaluated CSR bindings for browser-side API fetches
Pages get only the scripts they use: `nav.js` always, `reactive.js` when a page has a `data-scope`/CSR fetch, plus theme/validation/i18n/realtime runtimes when the relevant markup is present.
## Requirements / Notes
- **Bun-only.** Uses `Bun.serve` (HTTP + WebSocket), `Bun.file`, and `Bun.gzipSync`. The full app also relies on `bun:sqlite` / `Bun.SQL` via `@wrnexus/db`.
- Orchestrates the whole framework: `@wrnexus/core` (context, security, realtime registry), `@wrnexus/router`, `@wrnexus/ssr` (`renderDocument`), `@wrnexus/csr` (client runtimes), `@wrnexus/compiler` (`.wrn` → TS), `@wrnexus/styles`, `@wrnexus/ui`, `@wrnexus/validation`, `@wrnexus/i18n`, `@wrnexus/db`, and `@wrnexus/pubsub` (Redis-backed cross-process realtime).
- `.wrn` files compile into a content-addressed hidden `.wrnexus/` cache. Targeted
invalidation gives changed modules a fresh import identity without restarting
the development server.
- Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via `Cache-Control: no-transform`.
</content>
</invoke>
-70
View File
@@ -1,70 +0,0 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.16",
"type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/dev-server"
},
"homepage": "https://wrnexusjs.dev/packages/dev-server",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"dev-server"
],
"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"
},
"./serve-entry": {
"types": "./dist/serve-entry.d.ts",
"import": "./dist/serve-entry.js"
}
},
"dependencies": {
"@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",
"README.md"
]
}
-30
View File
@@ -1,30 +0,0 @@
# @wrnexus/dev-toolbar
Development-only page quality toolbar for WRNexusJS.
## Features
- Runtime, resource and unhandled promise error capture
- Accessibility, SEO, image, media, color, HTML, form, link, responsive and security checks
- Performance and network observations
- First-class application tabs for runtime, stores, cache, accessibility, SEO, performance,
security, images, links, and JavaScript
- Plugin-contributed applications with badges, descriptions, issue feeds, and structured data
- Element highlighting and issue filtering
- Server-side issue collector
- Development-only asset strings for direct serving by `@wrnexus/dev-server`
- Safe open-in-editor helper
## Dev-server integration
Serve `DEV_TOOLBAR_RUNTIME` at `/__wrnexus/dev-toolbar.js` and `DEV_TOOLBAR_CSS` at `/__wrnexus/dev-toolbar.css`, then inject this before `</body>` in development:
```html
<script type="module" src="/__wrnexus/dev-toolbar.js" data-wrnexus-dev-toolbar></script>
```
The browser runtime exposes `window.__wrnexusDevToolbar`.
Plugin panels returned through `devToolbarPanels()` are automatically added to the application
strip. Their issue category is filterable, and structured `data` is rendered as escaped diagnostic
content so a plugin never needs to inject toolbar HTML.
-59
View File
@@ -1,59 +0,0 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/dev-toolbar"
},
"homepage": "https://wrnexusjs.dev/packages/dev-toolbar",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"dev-toolbar"
],
"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"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"import": "./dist/server/index.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./rules": {
"types": "./dist/rules/index.d.ts",
"import": "./dist/rules/index.js"
}
},
"files": [
"dist",
"README.md"
]
}
-93
View File
@@ -1,93 +0,0 @@
# @wrnexus/i18n
Recursive locale loading, fallback resolution, SSR/browser translations, locale formatting, and language UI blocks for WRNexusJS.
## Locale files
Both layouts can be used together:
```text
app/locales/en.json
app/locales/en/common.json
app/locales/en/auth.json
app/locales/mr/common.json
```
Namespaced files become keys such as `common.save` and `auth.signIn`.
```ts
import { loadLocales, makeT, resolveI18n, resolveLang } from "@wrnexus/i18n";
const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
default: "en",
locales: ["en", "mr", "hi"],
fallbacks: { "mr-IN": ["mr", "en"] },
cookie: { name: "wrn-lang", sameSite: "Lax", secure: true },
});
const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
const t = makeT(i18n, lang);
t("common.hello", { name: "Ajay" });
```
## Resolution behavior
- normalized BCP-47-style locale names
- cookie preference
- weighted `Accept-Language`
- wildcard language ranges
- regional base fallback
- explicit fallback chains
- configured default language
- automatic RTL for Arabic, Hebrew, Persian, Urdu, and related languages
Locale JSON is size-limited and rejects prototype-pollution keys. Recursive namespace collisions are resolved safely.
## Views and runtime
```html
<h1 data-t="dashboard.title">Dashboard</h1>
<input t:placeholder="search.placeholder" />
```
Text and translated attributes are resolved during SSR. Active/fallback messages are serialized safely for the language runtime, which rebinds `data-t` markers after client navigation.
Enable `i18nPlugin()` to use:
- `<LanguageSwitcher />`
- `<LocaleStatus />`
`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.
## Formatting
- `formatNumber`
- `formatCurrency`
- `formatDate`
- `formatRelativeTime`
- `plural`
- `createLocaleFormatter`
- `translationCoverage`
Localization tooling can extract statically discoverable `t("key")`,
`i18n.t("key")`, and `data-i18n="key"` usage, compare every locale with a
reference, and create layout-stressing pseudo-locales:
```ts
import {
auditLocaleKeys,
createPseudoLocale,
extractTranslationKeysFromFiles,
} from "@wrnexus/i18n";
const used = extractTranslationKeysFromFiles(sourceFiles);
const coverage = auditLocaleKeys(messages, "en");
const enXA = createPseudoLocale(messages.en);
const arXB = createPseudoLocale(messages.en, { rtl: true });
```
Pseudo-localization preserves interpolation placeholders and markup tags. RTL
pseudo output uses Unicode direction controls, while runtime direction detection
continues to derive `rtl` from Arabic and other RTL language subtags.
-61
View File
@@ -1,61 +0,0 @@
{
"name": "@wrnexus/i18n",
"version": "0.8.9",
"type": "module",
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/i18n"
},
"homepage": "https://wrnexusjs.dev/packages/i18n",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"i18n"
],
"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"
},
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
},
"./components/*": "./components/*"
},
"dependencies": {
"@wrnexus/core": "^0.8.9",
"@wrnexus/plugin": "^0.8.8",
"@wrnexus/ui": "^0.8.12"
},
"wrnexus": {
"plugin": {
"plugin": "./dist/plugin.js",
"export": "default",
"factory": true
}
},
"files": [
"dist",
"README.md",
"components"
]
}
-134
View File
@@ -1,134 +0,0 @@
# @wrnexus/pubsub
> Topic-based publish/subscribe with a pluggable driver — in-process by default, Redis for cross-process messaging.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/pubsub` is a small server-side pub/sub bus. You publish messages to a
topic and subscribe with topic patterns; handlers fire for matching topics. The
default driver keeps everything in-process, and you can swap in the Redis driver
(`@wrnexus/pubsub/redis`) to fan messages out across processes or hosts. It also
backs `@wrnexus/core`'s realtime bridge for horizontal scaling.
## Installation
```bash
bun add @wrnexus/pubsub
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### `createPubSub(driver?): PubSub`
Creates a bus over a driver. Defaults to `memoryDriver()` (in-process).
```ts
interface PubSub {
publish<T = unknown>(topic: string, message: T): Promise<void>;
subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
close(): Promise<void>;
}
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;
```
- `publish(topic, message)` — resolves once the driver and in-memory async handlers finish.
- `subscribe(pattern, handler)` — returns an unsubscribe function.
- `close()` — idempotently rejects new work, clears local subscriptions, and closes the driver.
### Pattern matching
Subscription patterns match in three ways:
- **Exact** — `"order:created"` matches only that topic.
- **Prefix** — `"order:*"` matches any topic starting with `"order:"`.
- **Everything** — `"*"` matches all topics.
### `memoryDriver(): PubSubDriver`
The default in-process driver. Handlers are invoked synchronously (fire-and-forget
for async handlers) whenever a published topic matches a registered pattern.
```ts
interface PubSubDriver {
publish(topic: string, message: unknown): void | Promise<void>;
subscribe(pattern: string, handler: Handler): () => void;
}
```
### `@wrnexus/pubsub/redis` — `redisDriver(url?)`
A cross-process driver backed by Redis. It speaks RESP over a raw TCP socket via
`Bun.connect`, so it adds **no npm dependency**. `url` defaults to `$REDIS_URL`,
then `redis://localhost:6379`. The URL may carry a password and a database index
(e.g. `redis://:secret@host:6379/2`).
```ts
function redisDriver(url?: string, options?: RedisDriverOptions): PubSubDriver & { close(): void };
```
- Exact topics use Redis `SUBSCRIBE`; wildcard patterns (`ns:*`, `*`) use
`PSUBSCRIBE`, whose glob semantics line up with this library's matching.
- Messages are JSON-stringified on publish and `JSON.parse`d on receipt; a payload
that isn't valid JSON is delivered as the raw string.
- `close()` tears down both the subscriber and publisher connections.
- Lost sockets reconnect with bounded exponential backoff and active subscriptions
are replayed. `maxPending` bounds unavailable-connection writes (default 1000);
`reconnectDelayMs` and `reconnectMaxDelayMs` tune recovery (100ms/5000ms).
### RESP codec (internal)
`redis.ts` uses a minimal RESP implementation exported from `resp.ts`
(`encodeCommand`, `parseReply`, `concat`, and the `RespValue` type). These are
implementation details of the Redis driver, not part of the public package entry.
## Usage
In-process (default):
```ts
import { createPubSub } from "@wrnexus/pubsub";
const bus = createPubSub();
const off = bus.subscribe("order:*", (msg, topic) => {
console.log(topic, msg);
});
await bus.publish("order:created", { id: 7 });
off(); // unsubscribe
```
Cross-process with Redis:
```ts
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
const driver = redisDriver("redis://localhost:6379");
const bus = createPubSub(driver);
bus.subscribe("order:*", (msg, topic) => {
// received on any app process subscribed to this pattern
});
await bus.publish("order:created", { id: 7 });
// on shutdown (also closes the driver)
await bus.close();
```
## Requirements / Notes
- **Bun-only.** The Redis driver depends on `Bun.connect`; it throws
`redisDriver requires the Bun runtime (Bun.connect).` outside Bun. The default
in-memory driver has no runtime dependencies.
- The Redis driver reads `REDIS_URL` from the environment when no `url` is passed.
- Backs [`@wrnexus/core`](../core)'s realtime bridge for horizontal scaling.
- No external npm dependencies — the Redis client is a self-contained RESP codec.
-55
View File
@@ -1,55 +0,0 @@
{
"name": "@wrnexus/pubsub",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/pubsub"
},
"homepage": "https://wrnexusjs.dev/packages/pubsub",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"pubsub"
],
"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"
},
"./brokers": {
"types": "./dist/brokers.d.ts",
"import": "./dist/brokers.js"
},
"./redis": {
"types": "./dist/redis.d.ts",
"import": "./dist/redis.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.9",
"@wrnexus/rpc": "^0.8.9"
},
"files": [
"dist",
"README.md"
]
}
-174
View File
@@ -1,174 +0,0 @@
# @wrnexus/router
> File-based router that maps an `app/` directory onto route tables and matches request paths against them.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/router` scans an application's `app/` directory once at startup and builds route tables for pages, API endpoints, realtime channels, middleware, server-rendered `.wrn` components, layouts, and validation schemas. It also compiles URL patterns (`/users/[id]`) into RegExps and matches request paths against them. Request input is never turned into a file path, which makes the router immune to path traversal. This is a server-side package used by the WrNexus runtime to resolve incoming requests, plus a codegen helper for compile-time typed links.
## Installation
```bash
bun add @wrnexus/router
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## Directory conventions
The router maps files under `appDir` onto routes:
```
app/pages/index.tsx -> GET /
app/pages/about.tsx -> GET /about
app/pages/users/[id].tsx -> GET /users/:id
app/api/hello.ts -> /api/hello
app/realtime/chat.ts -> /realtime/chat
app/pages/*.wrn (api) -> embedded /api/* routes
app/pages/*.wrn (rt) -> embedded /realtime/* routes
app/middleware/*.ts -> global middleware (alphabetical)
app/components/*.wrn -> server-rendered components (by basename)
app/layouts/*.wrn -> named page layouts
app/schemas/*.ts -> validation schemas
```
Allowed route extensions are `.ts`, `.tsx`, and `.wrn`. Dotfiles and underscore-prefixed files are ignored. A trailing `index` segment is dropped from the route. `.wrn` pages may embed `api` and `realtime` blocks, which the router extracts and mounts under `/api/*` and `/realtime/*`.
## API
### `buildRouter(appDir, opts?): Router`
Scan an app directory and build all route tables.
```ts
function buildRouter(appDir: string, opts?: RouterOptions): Router;
interface RouterOptions {
/** Extra dirs scanned for `.wrn` components (e.g. `@wrnexus/ui`), before
* `app/components`, so an app component of the same name wins. */
componentDirs?: string[];
}
```
The returned `Router` exposes the built tables plus per-kind matchers:
```ts
interface Router {
pages: Route[];
api: Route[];
realtime: Route[];
/** Absolute paths of middleware modules, in execution order (alphabetical). */
middlewareFiles: string[];
/** Server-rendered `.wrn` components, mounted via `data-component`. */
components: ComponentRef[];
/** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
layouts: ComponentRef[];
/** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
schemas: ComponentRef[];
matchPage(pathname: string): RouteMatch | null;
matchApi(pathname: string): RouteMatch | null;
matchRealtime(pathname: string): RouteMatch | null;
}
interface ComponentRef {
/** Validated component name (matches a `data-component` attribute). */
name: string;
/** Absolute path to the component's `.wrn` module. */
file: string;
}
```
Component, layout, and schema names are validated with `isSafeIslandName` from `@wrnexus/core`; unsafe names are skipped with a warning. Realtime channel names are validated the same way.
### Route matching
| Export | Signature | Description |
| --------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `compileRoutePattern` | `(raw: string) => Pick<Route, "regex" \| "paramNames">` | Compile a `/users/[id]` pattern into a RegExp (with optional trailing slash) plus ordered param names. |
| `matchRoute` | `(routes: Route[], pathname: string) => RouteMatch \| null` | Return the first route whose regex matches; captured params are `decodeURIComponent`-decoded. |
| `sortRoutes` | `(routes: Route[]) => Route[]` | Order routes so static routes win over dynamic ones (fewer params first), then longer/more specific patterns first. |
```ts
interface Route {
raw: string; // e.g. "/users/[id]"
file: string; // absolute path to the handling module
regex: RegExp; // compiled matcher
paramNames: string[]; // ordered dynamic param names
}
interface RouteMatch {
route: Route;
params: Record<string, string>;
}
```
### Typed-routes codegen
```ts
function generateRoutesFile(pages: Route[]): string;
```
Emits the source for `app/routes.gen.ts`: a `Routes` map (each page path → its `[param]` types), a `RoutePath` union, and an `href()` builder that fills params and rejects unknown paths at compile time. Entries are de-duplicated and sorted by path.
### Re-exports
`Middleware` (the type from `@wrnexus/core`) is re-exported for callers that load middleware modules themselves.
## Usage
```ts
import { buildRouter } from "@wrnexus/router";
const router = buildRouter("./app", {
componentDirs: ["./node_modules/@wrnexus/ui/components"],
});
// Resolve an incoming request.
const match = router.matchPage("/users/42");
if (match) {
console.log(match.route.file); // absolute path to the page module
console.log(match.params); // { id: "42" }
}
const api = router.matchApi("/api/hello");
const rt = router.matchRealtime("/realtime/chat");
```
Generating the typed-routes file (as `wrnexus dev` does):
```ts
import { generateRoutesFile } from "@wrnexus/router";
import { writeFileSync } from "node:fs";
const router = buildRouter("./app");
writeFileSync("./app/routes.gen.ts", generateRoutesFile(router.pages));
```
```ts
// Then, in app code, links are checked at compile time:
import { href } from "./routes.gen.ts";
href("/users/[id]", { id: "42" }); // "/users/42"
href("/about"); // "/about"
href("/nope"); // type error: unknown path
```
Lower-level pattern matching, if you need it directly:
```ts
import { compileRoutePattern, matchRoute, sortRoutes, type Route } from "@wrnexus/router";
const { regex, paramNames } = compileRoutePattern("/posts/[slug]");
const routes = sortRoutes([{ raw: "/posts/[slug]", file: "…", regex, paramNames }]);
const m = matchRoute(routes, "/posts/hello"); // { route, params: { slug: "hello" } }
```
## Requirements / Notes
- Scanning uses `node:fs` (`existsSync`, `readdirSync`, `statSync`) and `node:path` — runs under Bun.
- Depends on [`@wrnexus/compiler`](../compiler) to `parse` `.wrn` pages and extract embedded `api` / `realtime` blocks.
- Depends on [`@wrnexus/core`](../core) for `isSafeIslandName` (name validation) and the `Middleware` type.
- Missing route directories are tolerated — a route kind you don't use simply yields an empty table.
-47
View File
@@ -1,47 +0,0 @@
{
"name": "@wrnexus/router",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/router"
},
"homepage": "https://wrnexusjs.dev/packages/router",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"router"
],
"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/compiler": "^0.8.9",
"@wrnexus/core": "^0.8.9"
},
"files": [
"dist",
"README.md"
]
}
-124
View File
@@ -1,124 +0,0 @@
# @wrnexus/ssr
> Server-side rendering: wraps a page's HTML body in a complete HTML document with a metadata-driven `<head>`.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
Pages in WrNexus return an HTML string for the body. `@wrnexus/ssr` takes that body and produces a full HTML document — building the `<head>` from page metadata and global SEO defaults, resolving canonical/Open Graph/Twitter tags, and injecting module preloads and `<script type="module">` tags. It is deliberately server-only: nothing in this package touches the DOM or ships to the browser, keeping server code genuinely server-only. Reach for it on the server when turning a rendered page body into a response document.
## Installation
```bash
bun add @wrnexus/ssr
```
> 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 export.
### `renderDocument(opts: RenderOptions): string`
Renders a complete HTML document as a string, beginning with `<!doctype html>`. All metadata is HTML-escaped (via `escapeHtml` from `@wrnexus/core`), so a malicious title or description cannot break out of its element or attribute. The body is placed inside `<div id="app">`.
#### `RenderOptions`
| Field | Type | Description |
| -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `meta` | `PageMeta` | Page metadata for the document head (required). |
| `body` | `string` | Rendered HTML for the body, placed inside `#app` (required). |
| `seo` | `SeoConfig` | Global SEO defaults, typically from `wrnexus.config.ts`. |
| `url` | `URL` | Current request URL, used to resolve canonical/Open Graph URLs. |
| `scripts` | `string[]` | URLs of `<script type="module">` tags to load (e.g. per-island chunks or the reactive runtime). Each also gets a `<link rel="modulepreload">`. |
| `defaultTitle` | `string` | Default document title used when `meta.title` is absent. |
| `extraHead` | `string` | Raw HTML injected at the end of `<head>` (trusted, framework-controlled — not escaped). |
| `extraBody` | `string` | Raw HTML injected at the end of `<body>` (trusted, framework-controlled — not escaped). |
| `htmlAttrs` | `string` | Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted). |
`PageMeta` and `SeoConfig` come from `@wrnexus/core`. `PageMeta` is an alias of `SeoConfig`, whose fields are all optional:
```ts
type SeoConfig = {
title?: string;
titleTemplate?: string; // e.g. "%s — My Site"; %s is replaced with the page title
description?: string;
canonical?: string;
canonicalBase?: string; // origin used to absolutize canonical/image URLs
robots?: string;
keywords?: string | string[];
image?: string;
siteName?: string;
type?: string; // Open Graph type; defaults to "website"
locale?: string;
twitterCard?: string; // defaults to "summary"
twitterSite?: string;
themeColor?: string;
};
```
#### Metadata resolution
`renderDocument` merges page metadata (`meta`) over global defaults (`seo`), field by field, so per-page values win. Notable behavior:
- **Title**: uses `meta.title`, else `seo.title`, else `defaultTitle`, else `"WrNexus"`. When the page sets its own title and `seo.titleTemplate` contains `%s`, the template is applied.
- **Canonical / image URLs**: resolved against `canonicalBase` (or the request `url`'s origin) into absolute URLs when possible.
- **Keywords**: an array is joined with `", "`.
- **Emitted tags**: `<title>`, and as applicable `description`, `robots`, `keywords`, `theme-color`, and `canonical` link, plus Open Graph (`og:title`, `og:description`, `og:type`, `og:url`, `og:site_name`, `og:locale`, `og:image`) and Twitter (`twitter:card`, `twitter:title`, `twitter:description`, `twitter:image`, `twitter:site`) meta tags. The document always includes `charset`, `viewport`, and a `/favicon.ico` icon link.
## Usage
### Render an SEO-ready application page
```ts
import { renderDocument } from "@wrnexus/ssr";
const html = renderDocument({
meta: {
title: "About Us",
description: "Learn more about our team.",
},
seo: {
titleTemplate: "%s — Acme",
siteName: "Acme",
canonicalBase: "https://acme.example",
twitterSite: "@acme",
},
url: new URL("https://acme.example/about"),
body: "<h1>About Us</h1>",
scripts: ["/_wrn/runtime.js", "/_wrn/islands/about.js"],
htmlAttrs: ' data-theme="dark"',
});
return new Response(html, {
headers: { "content-type": "text/html; charset=utf-8" },
});
```
The produced document has `<title>About Us — Acme</title>`, the SEO/Open Graph/Twitter tags derived from the merged metadata, a `modulepreload` link and module `<script>` for each entry in `scripts`, and the body wrapped in `<div id="app">`.
### Add trusted framework assets and boot data
Use `extraHead` and `extraBody` only for HTML generated by your application or the
framework. User-provided values belong in `meta`, where they are escaped.
```ts
const html = renderDocument({
meta: { title: "Dashboard", robots: "noindex" },
body: dashboardHtml,
url: ctx.url,
extraHead: '<link rel="stylesheet" href="/_wrnexus/admin.css">',
extraBody: `<script type="application/json" id="boot">${JSON.stringify(bootData).replaceAll("<", "\\u003c")}</script>`,
});
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
```
## Requirements / Notes
- **Server-only.** This module never imports or touches the DOM and is safe to keep out of client bundles.
- **Depends on [`@wrnexus/core`](../core)** for `escapeHtml` and the `PageMeta` / `SeoConfig` types.
- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime (Node is not supported).
-56
View File
@@ -1,56 +0,0 @@
{
"name": "@wrnexus/ssr",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/ssr"
},
"homepage": "https://wrnexusjs.dev/packages/ssr",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"ssr"
],
"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"
},
"./rpc": {
"types": "./dist/rpc.d.ts",
"import": "./dist/rpc.js"
},
"./store-context": {
"types": "./dist/store-context.d.ts",
"import": "./dist/store-context.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.9",
"@wrnexus/store": "^0.8.8",
"@wrnexus/security": "^0.8.8"
},
"files": [
"dist",
"README.md"
]
}
-273
View File
@@ -1,273 +0,0 @@
# @wrnexus/styles
## Reusable layers and presets
Compose local or package foundations in order; later layers override earlier
ones and the application has final base-config precedence:
```ts
export default defineConfig({
extends: ["@workroot/wrnexus-enterprise", "./layers/company"],
profiles: { production: { port: 8080 } },
});
```
A directory layer exports `wrnexus.layer.ts` (JavaScript/MJS are supported).
A package can provide that conventional file or declare
`wrnexus.layer` in its `package.json`. Layers may extend other layers and carry
the complete app configuration, including plugins that contribute layouts,
components, routes, middleware, and migrations. `plugins` and `head` compose;
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 `--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.
## Overview
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 (`--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.
## Installation
```bash
bun add @wrnexus/styles
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
Everything is exported from the package root (`@wrnexus/styles`).
### Config loading
| Export | Signature | Purpose |
| ---------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `loadAppConfig` | `(appRoot: string, profile?: string) => Promise<AppConfig>` | Load `wrnexus.config.*` with the active profile deep-merged in (`profiles` stripped from the result). |
| `loadRawConfig` | `(appRoot: string) => Promise<AppConfig>` | Load the raw config with the `profiles` map intact; returns `{}` if no config file exists. |
| `resolveProfile` | `(options?: { explicit?; mode? }) => string` | Resolve the active profile: explicit arg > `WRNEXUS_PROFILE` env var > mode-based default (`production` in prod, else `development`). |
| `loadEnv` | `(appRoot: string, profile: string) => Record<string, string>` | Load the `.env` cascade for a profile into `process.env` without clobbering real env vars. Returns what it loaded. |
| `headToString` | `(head?: string \| string[]) => string` | Flatten `AppConfig.head` into a single HTML string. |
Config file names probed, in order: `wrnexus.config.ts`, `wrnexus.config.js`, `wrnexus.config.mjs`.
`.env` cascade precedence (low → high): `.env` < `.env.<profile>` < `.env.local` < `.env.<profile>.local`. Variables already present in the real environment always win.
### `AppConfig`
The type of the object your `wrnexus.config.ts` default-exports. Every field is optional.
| Field | Type | Description |
| ----------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `head` | `string \| string[]` | Raw HTML appended to every page's `<head>` (e.g. CDN stylesheet/script links). |
| `seo` | `SeoConfig` | Global SEO defaults, merged with each page's exported `meta`. (from `@wrnexus/core`) |
| `security` | `SecurityConfig` | Framework security headers and optional CORS policy. (from `@wrnexus/core`) |
| `styles` | `StylesConfig` | Global stylesheet pipeline config (see below). |
| `theme` | `ThemeConfig` | Design-token themes, deep-merged over the built-in light/dark. |
| `i18n` | `{ default?: string; locales?: string[] }` | Default language + supported locales (strings live in `app/locales/*.json`). |
| `db` | `{ driver: "sqlite" \| "postgres" \| "mysql" \| "mongo"; url: string }` | Default database connection; reached with `getDb()`. |
| `databases` | `Record<string, { driver; url }>` | Additional named databases, reached with `getDb("<name>")`; each has its own `app/db/<name>/` migrations/queries. |
| `realtime` | `{ scale?: boolean; redisUrl?: string }` | When `scale` is true (or `redisUrl` is set), room broadcasts bridge over Redis pub/sub so they reach clients on every app process. |
| `port` | `number` | Default server port. |
| `profiles` | `Record<string, Partial<Omit<AppConfig, "profiles">>>` | Named profiles (dev, prod, uat, test, …). The active profile's overrides are deep-merged over the base config. Selected via `--profile=<name>` or `WRNEXUS_PROFILE`. |
### Styles pipeline
| Export | Signature | Purpose |
| ---------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `findStyleEntry` | `(appDir, appRoot, override?) => string \| null` | Resolve the CSS entry: `override` (relative to `appRoot`) → `app/styles/global.css` → an aggregate of all `app/styles/*.css` (written to `app/.wrnexus/styles-entry.css`). `null` if the app has no styles. |
| `bundleCss` | `(entryPath: string, mode: Mode) => Promise<string>` | Bundle an entry with `Bun.build` (CSS bundler). Resolves `@import` (local + node_modules), handles nesting, minifies when `mode === "production"`. |
| `renderStyles` | `(ctx: StyleProcessContext, styles?: StylesConfig) => Promise<string>` | Produce final CSS: runs `styles.process(ctx)` if provided, else `bundleCss`. Returns `""` when `ctx.entryPath` is null. |
`StylesConfig`:
```ts
interface StylesConfig {
/** CSS entry path relative to the app root. Default: app/styles/global.css */
entry?: string;
/** Custom processor — return the final CSS string (Tailwind/PostCSS/Sass). */
process?: (ctx: StyleProcessContext) => string | Promise<string>;
}
interface StyleProcessContext {
entryPath: string | null; // resolved absolute CSS entry, or null
appDir: string;
appRoot: string;
mode: Mode; // "development" | "production"
}
```
### Theme system
| Export | Type / Signature | Purpose |
| -------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `DEFAULT_THEMES` | `Record<string, ThemeTokens>` | Built-in `light` and `dark` token maps. |
| `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). |
| `resolveThemeName` | `(cookieValue: string \| undefined, theme: ResolvedTheme) => string` | Pick a valid theme name from a cookie, falling back to `theme.default`. |
| `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 `--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`:
```ts
type ThemeTokens = Record<string, string>;
interface ThemeConfig {
palette?: ThemePaletteName | CustomThemePalette;
default?: string; // theme used when no cookie is present
themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
}
interface ResolvedTheme {
default: string;
names: string[];
themes: Record<string, ThemeTokens>;
}
```
Built-in palettes are `blue`, `indigo`, `violet`, `emerald`, `cyan`, `rose`,
`amber`, and `slate`. Each supplies primary, secondary, info, success, warning,
danger/error, hover, and contrast colors to every light/dark theme. Built-in
token keys also include surfaces, text, borders, radii, fonts, and shadows.
A custom palette is intentionally complete, so components never fall back to an
unrelated blue status or action color:
```ts
theme: {
palette: {
primary: "#7c3aed",
primaryHover: "#6d28d9",
primaryContrast: "#ffffff",
secondary: "#db2777",
secondaryHover: "#be185d",
secondaryContrast: "#ffffff",
info: "#2563eb",
success: "#059669",
warning: "#d97706",
danger: "#dc2626",
},
}
```
The client runtime (`renderThemeRuntime`) exposes `window.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
### `wrnexus.config.ts`
```ts
import type { AppConfig } from "@wrnexus/styles";
export default {
head: [
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">',
],
port: 3000,
db: { driver: "sqlite", url: "app.db" },
theme: {
palette: "violet",
default: "dark",
themes: {
light: { "color-primary": "#7c3aed" }, // override one token; rest inherited
brand: {
// add a whole new theme
"color-scheme": "dark",
"color-bg": "#0a0a0a",
"color-primary": "#22d3ee",
},
},
},
styles: {
entry: "app/styles/main.css",
},
profiles: {
production: {
db: { driver: "postgres", url: process.env.DATABASE_URL! },
},
},
} satisfies AppConfig;
```
### Loading config + producing CSS
```ts
import {
loadAppConfig,
resolveProfile,
loadEnv,
findStyleEntry,
renderStyles,
} from "@wrnexus/styles";
const appRoot = process.cwd();
const mode = "production" as const;
const profile = resolveProfile({ mode });
loadEnv(appRoot, profile);
const config = await loadAppConfig(appRoot, profile);
const appDir = `${appRoot}/app`;
const entryPath = findStyleEntry(appDir, appRoot, config.styles?.entry);
const css = await renderStyles({ entryPath, appDir, appRoot, mode }, config.styles);
```
### Rendering the theme
```ts
import {
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderThemeRuntime,
THEME_COOKIE,
} from "@wrnexus/styles";
const theme = resolveThemeConfig(config.theme);
// Server: pick the active theme from the request cookie (no flash).
const active = resolveThemeName(cookies[THEME_COOKIE], theme);
// → render <html data-theme={active}>
const themeCss = renderThemeCss(theme); // served at THEME_CSS_HREF
const themeJs = renderThemeRuntime(theme); // served at THEME_JS_HREF
```
In templates, consume tokens via the custom properties:
```css
.card {
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-wrn-theme-toggle>Toggle theme</button>
<button data-wrn-theme-set="brand">Brand theme</button>
```
## Requirements / Notes
- **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 connected into pages by the framework's server; this package only produces their contents.
-48
View File
@@ -1,48 +0,0 @@
{
"name": "@wrnexus/styles",
"version": "0.8.10",
"type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/styles"
},
"homepage": "https://wrnexusjs.dev/packages/styles",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"styles"
],
"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/uploader": "^0.8.9",
"@wrnexus/core": "^0.8.9",
"@wrnexus/plugin": "^0.8.8"
},
"files": [
"dist",
"README.md"
]
}
-166
View File
@@ -1,166 +0,0 @@
# @wrnexus/test
> Testing utilities for WrNexus apps — component rendering, reactive-DOM mounting, route handler calls, and a full in-process app harness, plus a one-import re-export of `bun:test`.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/test` is the server-side test toolkit you reach for when writing tests
for a WrNexus app. It runs under `bun test` (invoked via `wrnexus test`) and gives
you a single import surface: the `bun:test` primitives (`test`, `expect`, `mock`,
…) re-exported alongside WrNexus-aware helpers that compile `.wrn` components,
hydrate server HTML in a DOM, invoke API route handlers, and boot the real app on
an ephemeral port for integration tests.
## Installation
```bash
bun add @wrnexus/test
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### Re-exported test primitives
For one-import DX, the following are re-exported straight from `bun:test`:
`test`, `expect`, `describe`, `it`, `beforeEach`, `afterEach`, `beforeAll`,
`afterAll`, `mock`, `spyOn`.
`createContext` is also re-exported from `@wrnexus/core`.
### `renderComponent(source, props?)`
```ts
function renderComponent(source: string, props?: Record<string, unknown>): Promise<string>;
```
Compiles a `.wrn` component `source` string (via `@wrnexus/compiler`) and renders
it to an HTML string with the given `props`. Throws if the compiled module has no
`render` export.
### `mountHtml(html)`
```ts
function mountHtml(html: string): {
document: Document;
window: unknown;
querySelector: (sel: string) => Element | null;
querySelectorAll: (sel: string) => Element[];
};
```
Mounts server-rendered `html` in a `happy-dom` window with the reactive runtime
hydrated, so you can test `data-scope` / `data-text` / `data-for` / `data-show`
behaviour. Returns the window plus `document` and query helpers; assert on those.
> `happy-dom` is loaded lazily (via `require`), so importing this package never
> requires it unless you actually call `mountHtml`.
### `callRoute(handler, request)`
```ts
function callRoute(
handler: (ctx: Context) => Response | Promise<Response>,
request: Request,
): Promise<Response>;
```
Calls an API route `handler` with a `Context` built from a `Request` (using
`createContext`). Returns the handler's `Response`.
### `createHarness(projectRoot, options?)`
```ts
function createHarness(projectRoot: string, options?: HarnessOptions): Promise<Harness>;
interface HarnessOptions {
/** Config/env profile. Default "test". */
profile?: string;
}
interface Harness {
/** Base URL of the ephemeral test server. */
url: string;
/** Fetch a path on the app (relative to `url`). */
fetch(path: string, init?: RequestInit): Promise<Response>;
/** The scanned router (pages/api/realtime/components). */
router: unknown;
/** Stop the server. */
close(): void;
}
```
Boots the app at `projectRoot` on an ephemeral port (`port: 0`) for integration
tests covering pages, API routes, middleware, and the full request pipeline. Loads
env and app config for the given `profile` (default `"test"`) so it picks up your
test database/env. The server runs in `development` mode with HMR disabled.
Remember to `await app.close()` when done.
## Usage
The CLI supports focused suites by file or directory convention:
```bash
wrnexus test unit # *.unit.test.ts or test/unit/**
wrnexus test component # *.component.test.ts or test/component/**
wrnexus test api # *.api.test.ts or test/api/**
wrnexus test accessibility # *.a11y.test.ts / *.accessibility.test.ts
wrnexus test performance # *.performance.test.ts / *.benchmark.test.ts
wrnexus test browser # Playwright project when configured
wrnexus test visual # Playwright tests tagged @visual
```
Pass the application directory after the level, for example
`wrnexus test component examples/basic-app`. A focused command fails clearly when no matching
suite exists instead of silently running unrelated tests.
```ts
import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";
test("counter renders its label", async () => {
const html = await renderComponent(SRC, { start: 3, label: "Hits" });
expect(html).toContain("Hits");
});
test("reactive scope hydrates", () => {
const { querySelector } = mountHtml(serverHtml);
expect(querySelector("[data-text]")?.textContent).toBe("3");
});
test("home page responds", async () => {
const app = await createHarness("examples/basic-app");
const res = await app.fetch("/");
expect(res.status).toBe(200);
await app.close();
});
```
Calling an API route handler directly:
```ts
import { test, expect, callRoute } from "@wrnexus/test";
import { GET } from "../app/api/health.ts";
test("health endpoint", async () => {
const res = await callRoute(GET, new Request("http://test/api/health"));
expect(res.status).toBe(200);
});
```
## Requirements / Notes
- **Bun-only.** Runs under `bun test` (via `wrnexus test`); uses Bun's module
loading and the `bun:test` runtime.
- `mountHtml` requires **`happy-dom`** to be available in the workspace (loaded
lazily; it's a dev dependency, not a runtime dependency of this package).
- Works with the rest of the WrNexus toolchain:
[`@wrnexus/compiler`](../compiler) (compiles `.wrn` sources),
[`@wrnexus/core`](../core) (`Context` / `createContext`),
[`@wrnexus/csr`](../csr) (reactive runtime for `mountHtml`),
[`@wrnexus/dev-server`](../dev-server) (`startServer` behind `createHarness`),
and [`@wrnexus/styles`](../styles) (config/env/profile loading for the harness).
-43
View File
@@ -1,43 +0,0 @@
{
"name": "@wrnexus/test",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/test"
},
"homepage": "https://wrnexusjs.dev/packages/test",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"test"
],
"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"
]
}
-943
View File
@@ -1,943 +0,0 @@
# WRNexus UI component reference
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
### AdvancedSelect
Theme-aware, responsive advanced select component.
- Mount: `data-component="AdvancedSelect"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Advanced Select"`, `name: string = ""`, `value: string = ""`, `values: unknown[] = []`, `options: unknown[] = []`, `groups: unknown[] = []`, `placeholder: string = "Select an option"`, `placeholderIcon: string = ""`, `searchPlaceholder: string = "Search options…"`, `multiple: boolean = false`, `searchable: boolean = true`, `defaultOpen: boolean = false`, `clearable: boolean = true`, `allowEmpty: boolean = true`, `tags: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `helpText: string = ""`, `loading: boolean = false`, `loadingLabel: string = "Loading options…"`, `emptyLabel: string = "No options found"`, `selectedOptionsLabel: string = "Selected options"`, `clearLabel: string = "Clear selection"`, `createLabel: string = "Create"`, `loadMoreLabel: string = "Load more"`, `searchMode: string = "contains"`, `searchFields: string = "label,description"`, `minSearchLength: number = 0`, `searchResultLimit: number = 0`, `maxSelections: number = 0`, `showCounter: boolean = false`, `counterTemplate: string = "{selected} selected"`, `optionTemplate: string = "default"`, `selectedTemplate: string = "default"`, `closeOnSelect: boolean = true`, `scrollToSelected: boolean = true`, `fixed: boolean = false`, `placement: string = "bottom"`, `remote: boolean = false`, `remoteUrl: string = ""`, `remoteQueryParam: string = "q"`, `remoteDebounce: number = 250`, `remoteAutoLoad: boolean = true`, `infinite: boolean = false`, `hasMore: boolean = false`, `page: number = 1`, `class: string = ""`
- Slots: None
- Outputs: `search({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### ComboBox
Editable autocomplete combobox with local and remote suggestions.
- Mount: `data-component="ComboBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "ComboBox"`, `name: string = ""`, `value: string = ""`, `options: unknown[] = []`, `groups: unknown[] = []`, `placeholder: string = "Search or select an option"`, `searchPlaceholder: string = "Start typing…"`, `clearable: boolean = true`, `allowCustomValue: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `helpText: string = ""`, `loading: boolean = false`, `loadingLabel: string = "Loading suggestions…"`, `emptyLabel: string = "No matching options"`, `clearLabel: string = "Clear value"`, `toggleLabel: string = "Toggle suggestions"`, `searchMode: string = "contains"`, `searchFields: string = "label,description"`, `minSearchLength: number = 0`, `searchResultLimit: number = 0`, `optionTemplate: string = "default"`, `defaultOpen: boolean = false`, `closeOnSelect: boolean = true`, `fixed: boolean = false`, `placement: string = "bottom"`, `autocomplete: string = "off"`, `remote: boolean = false`, `remoteUrl: string = ""`, `remoteQueryParam: string = "q"`, `remoteDebounce: number = 250`, `remoteAutoLoad: boolean = true`, `infinite: boolean = false`, `hasMore: boolean = false`, `page: number = 1`, `loadMoreLabel: string = "Load more"`, `class: string = ""`
- Slots: None
- Outputs: `search({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### InputNumber
Theme-aware, responsive input number component.
- Mount: `data-component="InputNumber"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`, `id: string = ""`, `name: string = "quantity"`, `value: number = 0`, `min: string = ""`, `max: string = ""`, `step: number = 1`, `precision: string = "auto"`, `label: string = ""`, `description: string = ""`, `helpText: string = ""`, `error: string = ""`, `invalid: boolean = false`, `prefix: string = ""`, `suffix: string = ""`, `placeholder: string = ""`, `autocomplete: string = "off"`, `inputMode: string = "decimal"`, `ariaLabel: string = ""`, `required: boolean = false`, `disabled: boolean = false`, `inputDisabled: boolean = false`, `buttonsDisabled: boolean = false`, `readonly: boolean = false`, `allowInput: boolean = true`, `keyboard: boolean = true`, `wheel: boolean = false`, `clamp: boolean = true`, `fullWidth: boolean = false`, `showButtons: boolean = true`, `showValidationMessage: boolean = true`, `decrementLabel: string = "Decrease value"`, `incrementLabel: string = "Increase value"`, `controlsLabel: string = "Quantity controls"`, `requiredMessage: string = "A value is required."`, `minMessage: string = "Value is below the minimum."`, `maxMessage: string = "Value is above the maximum."`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `increment({ value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `decrement({ value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### PinInput
Secure multi-cell PIN and verification-code input with regex and paste support.
- Mount: `data-component="PinInput"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Verification code"`, `name: string = "pin"`, `value: string = ""`, `length: number = 4`, `pattern: string = "[0-9]"`, `type: string = "text"`, `inputMode: string = "numeric"`, `placeholder: string = "○"`, `autocomplete: string = "one-time-code"`, `masked: boolean = false`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `autoFocus: boolean = false`, `autoSubmit: boolean = false`, `allowPaste: boolean = true`, `clearable: boolean = true`, `clearLabel: string = "Clear code"`, `separator: string = ""`, `groupSize: number = 0`, `helpText: string = ""`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `paste({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### StrongPassword
Theme-aware, responsive strong password component.
- Mount: `data-component="StrongPassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `change({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `strength({ value: string; score: number; maximumScore: number; percent: number; level: string })`
### ToggleCount
Theme-aware, responsive toggle count component.
- Mount: `data-component="ToggleCount"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "segmented"`, `class: string = ""`, `name: string = "billing-cycle"`, `value: string = "monthly"`, `firstValue: string = "monthly"`, `firstLabel: string = "Monthly"`, `secondValue: string = "annual"`, `secondLabel: string = "Annual"`, `ariaLabel: string = "Billing frequency"`, `items: unknown[] = []`, `currency: string = "$"`, `suffix: string = ""`, `firstValueKey: string = "monthly"`, `secondValueKey: string = "annual"`, `emptyValue: string = "—"`, `align: string = "end"`, `fullWidth: boolean = true`, `disabled: boolean = false`, `animate: boolean = true`, `animationDuration: number = 450`, `animationSteps: number = 18`
- Slots: None
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### TogglePassword
Accessible password field with optional show and hide controls.
- Mount: `data-component="TogglePassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Enter your password"`, `autocomplete: string = "current-password"`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"`, `fields: unknown[] = []`, `visible: boolean = false`, `toggleable: boolean = true`, `toggleMode: string = "button"`, `checkboxLabel: string = "Show password"`, `showLabel: string = "Show password"`, `hideLabel: string = "Hide password"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `helpText: string = ""`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ name?: string; value: string | number | boolean | null | object; visible: boolean })`, `change({ name?: string; value: string | number | boolean | null | object; visible: boolean })`, `toggle({ visible: boolean })`
## Base
### Accordion
Theme-aware, responsive accordion component.
- Mount: `data-component="Accordion"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`, `id: string = "accordion"`, `items: unknown[] = []`, `defaultOpen: unknown[] = []`, `multiple: boolean = false`, `alwaysOpen: boolean = false`, `disabled: boolean = false`, `indicator: string = "plus"`, `indicatorPosition: string = "start"`, `showIndicator: boolean = true`, `bordered: boolean = false`, `separated: boolean = false`, `flush: boolean = false`, `contentItalic: boolean = false`
- Slots: None
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Alert
Theme-aware, responsive alert component.
- Mount: `data-component="Alert"`
- Props: `size: string = "default"`, `color: string = "info"`, `variant: string = "soft"`, `class: string = ""`, `radius: string = "md"`, `shadow: string = "sm"`, `title: string = "Alert"`, `description: string = ""`, `items: unknown[] = []`, `actions: unknown[] = []`, `showIcon: boolean = false`, `icon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss alert"`, `role: string = "alert"`, `live: string = "polite"`, `linkLabel: string = ""`, `linkHref: string = ""`, `actionLabel: string = ""`, `actionHref: string = ""`, `compact: boolean = false`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Avatar
Theme-aware, responsive avatar component.
- Mount: `data-component="Avatar"`
- Props: `src: string = ""`, `alt: string = ""`, `initials: string = ""`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "circle"`, `status: string = ""`, `statusLabel: string = ""`, `statusPosition: string = "bottom"`, `badge: string = ""`, `badgeIcon: string = ""`, `badgeLabel: string = ""`, `tooltip: string = ""`, `name: string = ""`, `description: string = ""`, `loading: string = "lazy"`, `class: string = ""`
- Slots: None
- Outputs: `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`, `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### AvatarGroup
Theme-aware, responsive avatar group component.
- Mount: `data-component="AvatarGroup"`
- Props: `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "circle"`, `layout: string = "stack"`, `maxVisible: number = 4`, `columns: number = 3`, `borderColor: string = ""`, `showTooltips: boolean = true`, `overflowLabel: string = "Show remaining members"`, `class: string = ""`
- Slots: None
- Outputs: `overflow({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Badge
Theme-aware, responsive badge component.
- Mount: `data-component="Badge"`
- Props: `label: string = "Badge"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "pill"`, `class: string = ""`, `icon: string = ""`, `iconPosition: string = "start"`, `dot: boolean = false`, `dotOnly: boolean = false`, `dotLabel: string = "Status"`, `animated: boolean = false`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Remove badge"`, `truncate: boolean = false`, `maxWidth: string = "12rem"`, `anchorLabel: string = ""`, `anchorIcon: string = ""`, `placement: string = "inline"`, `anchorLabelText: string = "Badge anchor"`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Blockquote
Theme-aware, responsive blockquote component.
- Mount: `data-component="Blockquote"`
- Props: `quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."`, `citation: string = ""`, `citationTitle: string = ""`, `citationUrl: string = ""`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `size: string = "md"`, `color: string = "primary"`, `align: string = "left"`, `variant: string = "default"`, `quoteMark: boolean = true`, `italic: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Button
Theme-aware, responsive button component.
- Mount: `data-component="Button"`
- Props: `label: string = "Button"`, `loadingLabel: string = "Loading…"`, `description: string = ""`, `as: string = ""`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `type: string = "button"`, `variant: string = "default"`, `color: string = "primary"`, `size: string = "default"`, `disabled: boolean = false`, `loading: boolean = false`, `pill: boolean = false`, `fullWidth: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `ariaLabel: string = ""`, `ariaPressed: string = ""`, `ariaExpanded: string = ""`, `ariaControls: string = ""`, `title: string = ""`, `autofocus: boolean = false`, `controlClass: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ButtonGroup
Theme-aware, responsive button group component.
- Mount: `data-component="ButtonGroup"`
- Props: `items: unknown[] = []`, `value: string = ""`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `orientation: string = "horizontal"`, `responsive: boolean = false`, `attached: boolean = true`, `selectable: boolean = false`, `toolbar: boolean = false`, `disabled: boolean = false`, `ariaLabel: string = "Button group"`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`, `change({ value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`
### Card
Group related content in a responsive themed surface with title, description, content, and supporting slots.
- Mount: `data-component="Card"`
- Props: `title: string = "Card title"`, `subtitle: string = ""`, `description: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `navigate({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### Carousel
Theme-aware, responsive carousel component.
- Mount: `data-component="Carousel"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = ""`, `description: string = ""`, `items: unknown[] = []`, `activeIndex: number = 0`, `slidesPerView: number = 1`, `gap: string = "0.75rem"`, `showPagination: boolean = false`, `isAutoPlay: boolean = false`, `autoplayInterval: number = 4000`, `isInfiniteLoop: boolean = false`, `isRTL: boolean = false`, `isCentered: boolean = false`, `isDraggable: boolean = false`, `isAutoHeight: boolean = false`, `isSnap: boolean = false`, `showCounter: boolean = false`, `thumbnails: string = "none"`, `ariaLabel: string = "Content carousel"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `initialize({ index: number; count: number })`, `change({ index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })`, `previous({ index: number; previousIndex: number; item: string | number | boolean | null | object })`, `next({ index: number; previousIndex: number; item: string | number | boolean | null | object })`, `play({ index: number; interval: number })`, `pause({ index: number })`, `reachStart({ index: number })`, `reachEnd({ index: number })`, `dragStart({ index: number; x: null })`, `dragEnd({ index: number; distance: number })`
### ChatBubble
Theme-aware, responsive chat bubble component.
- Mount: `data-component="ChatBubble"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = ""`, `description: string = ""`, `items: unknown[] = []`, `oneSided: boolean = false`, `showAvatars: boolean = false`, `showMetadata: boolean = false`, `ariaLabel: string = "Conversation"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `action({ action: boolean; item: string | number | boolean | null | object; index: number })`, `messageClick({ item: string | number | boolean | null | object; index: number; direction: string })`, `avatarClick({ item: string | number | boolean | null | object; index: number; direction: string })`, `linkClick({ link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`
### Collapse
Theme-aware, responsive collapse component.
- Mount: `data-component="Collapse"`
- Props: `size: string = "default"`, `color: string = "primary"`, `items: unknown[] = []`, `multiple: boolean = false`, `mode: string = "panel"`, `initialOpenIndexes: unknown[] = []`, `ariaLabel: string = "Collapsible content"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })`, `open({ index: number; item: string | number | boolean | null | object })`, `close({ index: number; item: string | number | boolean | null | object })`
### DatePicker
Theme-aware, responsive date picker component.
- Mount: `data-component="DatePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Date Picker"`, `id: string = ""`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "date"`, `locale: string = "en-US"`, `firstDayOfWeek: number = 0`, `months: string = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]`, `days: string = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]`, `years: string = [2024, 2025, 2026, 2027, 2028, 2029, 2030]`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `variant: string = "normal"`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `open({ value: string; name: string; sourceEvent: Event })`, `close({ value: string; name: string; sourceEvent: Event })`, `focus({ value: string; name: string; sourceEvent: Event })`, `blur({ value: string; name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
### DeviceFrame
Theme-aware, responsive device frame component.
- Mount: `data-component="DeviceFrame"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Device Frame"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `device: string = "phone"`, `orientation: string = "portrait"`, `src: string = ""`, `srcdoc: string = ""`, `frameTitle: string = "Device preview"`, `showToolbar: boolean = true`, `allow: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ device: string; orientation: string; sourceEvent: Event })`, `rotate({ device: string; orientation: string; sourceEvent: Event })`
### FileUploadProgress
Theme-aware, responsive file upload progress component.
- Mount: `data-component="FileUploadProgress"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Progress"`, `value: number = 50`, `max: number = 100`, `showValue: boolean = true`, `fileName: string = ""`, `fileSize: string = ""`, `uploadedSize: string = ""`, `status: string = "uploading"`, `cancelLabel: string = "Cancel upload"`, `retryLabel: string = "Retry upload"`, `class: string = ""`
- Slots: None
- Outputs: `cancel({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `retry({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `complete({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### LegendIndicator
Theme-aware, responsive legend indicator component.
- Mount: `data-component="LegendIndicator"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Legend Indicator"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### List
Present structured responsive linked or status items with icons, descriptions, actions, and selection events.
- Mount: `data-component="List"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ListGroup
Theme-aware, responsive list group component.
- Mount: `data-component="ListGroup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List Group"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Marquee
Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior.
- Mount: `data-component="Marquee"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `pause({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `resume({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Progress
Theme-aware, responsive progress component.
- Mount: `data-component="Progress"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Progress"`, `value: number = 50`, `max: number = 100`, `showValue: boolean = true`, `class: string = ""`
- Slots: None
- Outputs: None
### Rating
Theme-aware, responsive rating component.
- Mount: `data-component="Rating"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Rating"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Skeleton
Theme-aware, responsive skeleton component.
- Mount: `data-component="Skeleton"`
- Props: `color: string = "primary"`, `label: string = "Loading"`, `size: string = "md"`, `lines: number = 3`, `class: string = ""`
- Slots: None
- Outputs: None
### Spinner
Theme-aware, responsive spinner component.
- Mount: `data-component="Spinner"`
- Props: `color: string = "primary"`, `label: string = "Loading"`, `size: string = "md"`, `lines: number = 3`, `class: string = ""`
- Slots: None
- Outputs: None
### StyledIcon
Theme-aware, responsive styled icon component.
- Mount: `data-component="StyledIcon"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Styled Icon"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Timeline
Present responsive chronological activity, milestones, or workflow status with rich item metadata.
- Mount: `data-component="Timeline"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### TreeView
Theme-aware, responsive tree view component.
- Mount: `data-component="TreeView"`
- 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({ 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
### AuthForm
Reusable auth form component.
- Mount: `data-component="AuthForm"`
- Props: `size: string = "default"`, `color: string = "primary"`, `mode: string = "sign-in"`, `action: string = "/api/auth/login"`, `method: string = "post"`, `title: string = "Sign in"`, `description: string = ""`, `returnTo: string = ""`, `schema: string = ""`, `showRemember: boolean = true`, `showName: boolean = true`, `submitLabel: string = "Continue"`, `class: string = ""`
- Slots: `default`
- Outputs: `submit({ event: Event; mode: string; action: string })`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### AuthSplitLayout
Reusable auth split layout component.
- Mount: `data-component="AuthSplitLayout"`
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Secure identity"`, `title: string = "Welcome back"`, `description: string = ""`, `brand: string = "Police Management System"`, `features: unknown[] = []`, `class: string = ""`
- Slots: `aside-extra`, `form`
- Outputs: None
### PortalDashboard
Reusable portal dashboard component.
- Mount: `data-component="PortalDashboard"`
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Overview"`, `eyebrowKey: string = ""`, `title: string = "Dashboard"`, `titleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `userName: string = ""`, `metrics: unknown[] = []`, `actions: unknown[] = []`, `updates: unknown[] = []`, `tasks: unknown[] = []`, `class: string = ""`
- Slots: `hero-action`
- Outputs: `action({ item: string | number | boolean | null | object; value: string | number | boolean; index: number })`, `navigate({ item: string | number | boolean | null | object; value: string | number | boolean; index: number })`
### PreferenceSwitcher
Reusable preference switcher component.
- Mount: `data-component="PreferenceSwitcher"`
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
- Slots: None
- Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Toaster
Reusable toaster component.
- Mount: `data-component="Toaster"`
- Props: `color: string = "info"`, `size: string = "default"`, `position: string = "bottom-right"`, `duration: number = 4500`, `max: number = 4`, `pauseOnHover: boolean = true`, `showIcon: boolean = true`, `successIcon: string = ""`, `dangerIcon: string = ""`, `warningIcon: string = ""`, `infoIcon: string = ""`, `closable: boolean = true`, `showProgress: boolean = true`, `closeLabel: string = "Dismiss notification"`, `class: string = ""`
- Slots: None
- Outputs: `show({ id: number; message: string; tone: string })`, `dismiss({ id: number; reason: string })`, `action({ id: number; sourceEvent: Event })`
## Data
### MetricCard
Display one operational metric with value, suffix, description, icon, trend, progress, and optional action.
- Mount: `data-component="MetricCard"`
- Props: `label: string = "Metric"`, `value: string = "0"`, `description: string = ""`, `icon: string = ""`, `iconStyle: string = "soft"`, `prefix: string = ""`, `suffix: string = ""`, `badge: string = ""`, `trend: string = ""`, `trendLabel: string = ""`, `trendDirection: string = "neutral"`, `progress: number = -1`, `progressLabel: string = ""`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = ""`, `actionIcon: string = ""`, `showArrow: boolean = true`, `selectable: boolean = false`, `disabled: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `class: string = ""`
- Slots: `default`, `footer`
- Outputs: `select({ label: string; value: string; href: string; sourceEvent: Event })`, `action({ label: string; value: string; href: string; sourceEvent: Event })`
### MetricGrid
Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid.
- Mount: `data-component="MetricGrid"`
- Props: `items: unknown[] = []`, `columns: number = 4`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `equalHeight: boolean = true`, `dividers: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "full"`, `minItemWidth: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })`
### StatsBar
Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
- Mount: `data-component="StatsBar"`
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: None
- Outputs: None
## Forms
### Checkbox
Theme-aware, responsive checkbox component.
- Mount: `data-component="Checkbox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Checkbox"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `values: unknown[] = []`, `options: unknown[] = []`, `checked: boolean = false`, `indeterminate: boolean = false`, `orientation: string = "vertical"`, `card: boolean = false`, `rightAligned: boolean = false`, `list: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ColorPicker
Theme-aware, responsive color picker component.
- Mount: `data-component="ColorPicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Color"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = "#2563eb"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### FileInput
Theme-aware, responsive file input component.
- Mount: `data-component="FileInput"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "File"`, `hiddenLabel: boolean = false`, `placeholder: string = "Choose a file"`, `value: string = ""`, `icon: string = "icon-[lucide--upload]"`, `iconPosition: string = "start"`, `accept: string = ""`, `multiple: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ files: File[]; name: string; sourceEvent: Event })`, `change({ files: File[]; name: string; sourceEvent: Event })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ files: File[]; name: string; sourceEvent: Event })`, `clear({ name: string; sourceEvent: Event })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Input
Theme-aware, responsive input component.
- Mount: `data-component="Input"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `type: string = "text"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `autocomplete: string = ""`, `inputmode: string = ""`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = ""`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `blur({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`, `keydown({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `keyup({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`
### InputGroup
Theme-aware, responsive input group component.
- Mount: `data-component="InputGroup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input group"`, `hiddenLabel: boolean = false`, `value: string = ""`, `placeholder: string = ""`, `type: string = "text"`, `startText: string = ""`, `endText: string = ""`, `icon: string = ""`, `iconPosition: string = "start"`, `actionLabel: string = ""`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `submit({ value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `action({ name: string; sourceEvent: Event })`
### Radio
Theme-aware, responsive radio component.
- Mount: `data-component="Radio"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Radio"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `options: unknown[] = []`, `checked: boolean = false`, `orientation: string = "vertical"`, `card: boolean = false`, `rightAligned: boolean = false`, `list: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### RangeSlider
Theme-aware, responsive range slider component.
- Mount: `data-component="RangeSlider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Range"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: number = 50`, `min: number = 0`, `max: number = 100`, `step: number = 1`, `showValue: boolean = true`, `showBounds: boolean = true`, `showSteps: boolean = false`, `marks: unknown[] = []`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input(({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))`, `change(({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### SearchBox
Provide an accessible responsive search field with labels, validation states, sizes, and input or change events.
- Mount: `data-component="SearchBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `search({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Select
Theme-aware, responsive select component.
- Mount: `data-component="Select"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Select"`, `hiddenLabel: boolean = false`, `value: string = ""`, `values: unknown[] = []`, `options: unknown[] = []`, `placeholder: string = "Select an option"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `multiple: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `open({ name: string; sourceEvent: Event })`, `close({ name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
### Switch
Theme-aware, responsive switch component.
- Mount: `data-component="Switch"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Switch"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `checked: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Textarea
Theme-aware, responsive textarea component.
- Mount: `data-component="Textarea"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Textarea"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `rows: number = 5`, `resize: string = "vertical"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `minlength: string = ""`, `maxlength: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`
### TimePicker
Theme-aware, responsive time picker component.
- Mount: `data-component="TimePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Time"`, `hiddenLabel: boolean = false`, `value: string = ""`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = "icon-[lucide--clock-3]"`, `iconPosition: string = "end"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `format: string = "24"`, `minuteStep: number = 5`, `hours: string = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]`, `minutes: string = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"]`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ value: string; name: string; sourceEvent: Event })`, `blur({ value: string; name: string; sourceEvent: Event })`, `open({ value: string; name: string; sourceEvent: Event })`, `close({ value: string; name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
## Integrations
### Chart
Theme-aware, responsive chart component.
- Mount: `data-component="Chart"`
- 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({ 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: `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({ 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: `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; duration: number })`, `complete({ sourceEvent?: Event; duration: number })`
### DataMap
Theme-aware, responsive data map component.
- Mount: `data-component="DataMap"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Data Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### DragAndDrop
Theme-aware, responsive drag and drop component.
- Mount: `data-component="DragAndDrop"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Drag And Drop"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `dragStart({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragEnd({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragEnter({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragLeave({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `drop({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Map
Present responsive location information and markers with map-ready metadata and movement or marker events.
- Mount: `data-component="Map"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `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
Theme-aware, responsive wysiwyg editor component.
- Mount: `data-component="WysiwygEditor"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Wysiwyg Editor"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Layout
### Columns
Create responsive balanced content columns with configurable count, gap, density, and maximum width.
- Mount: `data-component="Columns"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Container
Constrain and align page content with responsive gutters and compact, wide, or full width options.
- Mount: `data-component="Container"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `centered: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: None
### CustomScrollbar
Theme-aware, responsive custom scrollbar component.
- Mount: `data-component="CustomScrollbar"`
- Props: `color: string = "primary"`, `size: string = "default"`, `axis: string = "vertical"`, `thickness: number = 8`, `maxHeight: string = "20rem"`, `radius: string = "999px"`, `class: string = ""`
- Slots: `default`
- 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"`, `variant: string = "solid"`, `class: string = ""`
- Slots: None
- Outputs: None
### FeatureGrid
Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid.
- Mount: `data-component="FeatureGrid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `items: unknown[] = []`, `columns: number = 3`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `minItemWidth: string = ""`, `equalHeight: boolean = true`, `align: string = "stretch"`, `maxWidth: string = "full"`, `variant: string = "default"`, `label: string = "Features"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Footer
Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
- Mount: `data-component="Footer"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Grid
Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.
- Mount: `data-component="Grid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `minItemWidth: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Image
Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.
- Mount: `data-component="Image"`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `fit: string = "cover"`, `rounded: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Kbd
Theme-aware, responsive kbd component.
- Mount: `data-component="Kbd"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### LayoutSplitter
Theme-aware, responsive layout splitter component.
- Mount: `data-component="LayoutSplitter"`
- Props: `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 = ""`, `underline: string = "hover"`, `external: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### PublicPageShell
Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages.
- Mount: `data-component="PublicPageShell"`
- Props: `maxWidth: string = "full"`, `fullWidth: boolean = true`, `headerOffset: string = "none"`, `background: string = "default"`, `overflow: string = "clip"`, `minHeight: string = "screen"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `before`, `default`, `after`
- Outputs: None
### Section
Create a responsive themed page section with controlled spacing, width, borders, and surface treatment.
- Mount: `data-component="Section"`
- Props: `id: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `spacing: string = "lg"`, `maxWidth: string = "xl"`, `fullWidth: boolean = false`, `borderTop: boolean = false`, `borderBottom: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### SectionHeader
Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy.
- Mount: `data-component="SectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `headingLevel: number = 2`, `maxWidth: string = "3xl"`, `class: string = ""`
- Slots: `icon`, `default`, `actions`
- Outputs: None
### Typography
Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy.
- Mount: `data-component="Typography"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
## Marketing
### AnnouncementBar
Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
- Mount: `data-component="AnnouncementBar"`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### CTASection
Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
- Mount: `data-component="CTASection"`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
- Slots: `default`, `actions`, `visual`, `footer`
- Outputs: None
### FeatureCard
Present one linked feature or service with media, icon, badge, description, and action.
- Mount: `data-component="FeatureCard"`
- Props: `icon: string = ""`, `iconStyle: string = "soft"`, `iconSize: string = "default"`, `eyebrow: string = ""`, `title: string = "Feature"`, `description: string = ""`, `image: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `imageAspect: string = "wide"`, `imageLoading: string = "lazy"`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = "Learn more"`, `actionIcon: string = ""`, `showArrow: boolean = true`, `stretchedLink: boolean = true`, `badge: string = ""`, `badgeColor: string = "primary"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `disabled: boolean = false`, `class: string = ""`
- Slots: `media`, `icon`, `default`, `footer`
- Outputs: None
### FeatureIconCard
Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
- Mount: `data-component="FeatureIconCard"`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
- Slots: `default`, `footer`
- Outputs: None
### Hero
Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel.
- Mount: `data-component="Hero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = ""`, `icon: string = ""`, `title: string = "Build something remarkable"`, `highlight: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "split"`, `visualPosition: string = "right"`, `visualStyle: string = "plain"`, `showDecorations: boolean = true`, `fullBleed: boolean = true`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualIcon: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualItems: unknown[] = []`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `primaryTarget: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `secondaryTarget: string = ""`, `tertiaryLabel: string = ""`, `tertiaryHref: string = ""`, `tertiaryIcon: string = ""`, `tertiaryTarget: string = ""`, `badges: unknown[] = []`, `trustItems: unknown[] = []`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `eyebrow`, `actions`, `trust`, `default`, `visual`, `footer`
- Outputs: None
### HeroActions
Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking.
- Mount: `data-component="HeroActions"`
- Props: `actions: unknown[] = []`, `align: string = "left"`, `orientation: string = "horizontal"`, `stackOnMobile: boolean = true`, `fullWidthMobile: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### MarketingSectionHeader
Introduce marketing content with an eyebrow, title, description, and optional linked action.
- Mount: `data-component="MarketingSectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "split"`, `size: string = "default"`, `color: string = "primary"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `actionExternal: boolean = false`, `class: string = ""`
- Slots: `icon`, `actions`, `default`
- Outputs: None
### PageHeader
Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions.
- Mount: `data-component="PageHeader"`
- Props: `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `id: string = "page-title"`, `align: string = "left"`, `centered: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `maxWidth: string = "xl"`, `showBreadcrumbs: boolean = false`, `breadcrumbs: unknown[] = []`, `breadcrumbParent: string = ""`, `breadcrumbParentHref: string = ""`, `breadcrumbCurrent: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `color: string = "primary"`, `variant: string = "default"`, `borderBottom: boolean = true`, `class: string = ""`
- Slots: `meta`, `actions`, `default`
- Outputs: None
### SplitHero
Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
- Mount: `data-component="SplitHero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
- Slots: `default`, `actions`, `trust`, `visual`
- Outputs: None
### TextLink
Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling.
- Mount: `data-component="TextLink"`
- Props: `label: string = "Learn more"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `showArrow: boolean = true`, `underline: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `disabled: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Navigation
### BackToTop
Provide a responsive floating control that returns long pages to the top and can show scroll progress.
- Mount: `data-component="BackToTop"`
- Props: `threshold: number = 500`, `label: string = "Back to top"`, `ariaLabel: string = "Scroll back to top"`, `icon: string = "icon-[lucide--arrow-up]"`, `position: string = "right"`, `offset: string = "md"`, `behavior: string = "smooth"`, `showProgress: boolean = false`, `showLabel: boolean = false`, `alwaysVisible: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "round"`, `class: string = ""`
- Slots: None
- Outputs: None
### Breadcrumb
Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
- Mount: `data-component="Breadcrumb"`
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
- Slots: None
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### MegaMenu
Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"`
- Props: `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 })`, `close({ reason: string })`, `select({ item: object; value: string })`
### Nav
Theme-aware, responsive nav component.
- Mount: `data-component="Nav"`
- 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({ item: object; value: string })`
### Navbar
Theme-aware, responsive navbar component.
- Mount: `data-component="Navbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
- Slots: `topbar`, `actions`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`, `action({ item: string | number | boolean | null | object; value: string | number | boolean })`
### Pagination
Theme-aware, responsive pagination component.
- Mount: `data-component="Pagination"`
- Props: `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({ page: number; pageSize: number })`, `previous({ page: number })`, `next({ page: number })`
### Scrollspy
Theme-aware, responsive scrollspy component.
- Mount: `data-component="Scrollspy"`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ href: string; label: string })`
### Sidebar
Theme-aware, responsive sidebar component.
- Mount: `data-component="Sidebar"`
- 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: `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: `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
### ContextMenu
Open an accessible keyboard-aware action menu from pointer or keyboard context interactions.
- Mount: `data-component="ContextMenu"`
- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `trigger: string = "contextmenu"`, `placement: string = "pointer"`, `align: string = "start"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `title: string = ""`, `description: string = ""`, `label: string = "Context menu"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `minWidth: string = "14rem"`, `maxWidth: string = "20rem"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ x: number; y: number; trigger: string; sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `select({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`
### Drawer
Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events.
- Mount: `data-component="Drawer"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `duration: number = 260`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ placement: string; sourceEvent: Event })`, `close({ reason: string; placement: string; sourceEvent: Event })`, `cancel({ placement: string; sourceEvent: Event })`
### Dropdown
Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement.
- Mount: `data-component="Dropdown"`
- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `label: string = "Open menu"`, `icon: string = ""`, `showChevron: boolean = true`, `menuLabel: string = "Dropdown menu"`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `emptyLabel: string = "No menu items"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `toggle({ open: boolean; sourceEvent: Event; reason?: object })`, `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `select({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`
### Modal
Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing.
- Mount: `data-component="Modal"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `scrollBehavior: string = "inside"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `cancel({ sourceEvent: Event })`, `confirm({ sourceEvent: Event })`
### Popover
Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events.
- Mount: `data-component="Popover"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `triggerLabel: string = "Open popover"`, `triggerIcon: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `showArrow: boolean = true`, `showClose: boolean = false`, `closeLabel: string = "Close popover"`, `closeOnOutside: boolean = true`, `closeOnEscape: boolean = true`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `closeOnAction: boolean = true`, `disabled: boolean = false`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `toggle({ open: boolean; sourceEvent: Event; reason?: object })`, `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `action({ href: string; sourceEvent: Event })`
### Tooltip
Show concise accessible contextual help on hover, focus, click, or controlled open state.
- Mount: `data-component="Tooltip"`
- Props: `id: string = ""`, `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = ""`, `description: string = ""`, `content: string = "Tooltip"`, `trigger: string = "hover"`, `placement: string = "top"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "dark"`, `maxWidth: string = "18rem"`, `offset: number = 10`, `showArrow: boolean = true`, `interactive: boolean = false`, `disabled: boolean = false`, `class: string = ""`
- Slots: `trigger`, `default`
- Outputs: `toggle({ open: boolean; reason: string; sourceEvent: Event })`, `open({ reason: string; sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`
## Tables
### DataTable
Sortable, filterable, paginated data table with row selection.
- Mount: `data-component="DataTable"`
- Props: `color: string = "primary"`, `size: string = "default"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `rowKey: string = "id"`, `remote: boolean = false`, `loadingLabel: string = "Loading"`, `errorLabel: string = "Could not load this data"`, `retryLabel: string = "Try again"`, `caption: string = ""`, `description: string = ""`, `searchable: boolean = true`, `searchPlaceholder: string = "Search"`, `paginated: boolean = true`, `pageSize: number = 10`, `paginationStyle: string = "compact"`, `pageSizes: number[] = [10, 25, 50]`, `selectable: boolean = false`, `actions: unknown[] = []`, `striped: boolean = true`, `bordered: boolean = true`, `gridlines: string = "rows"`, `density: string = "default"`, `emptyLabel: string = "No records to show"`, `noResultsLabel: string = "No records match your search"`, `clearSearchLabel: string = "Clear search"`, `stickyFirstColumn: boolean = false`, `layout: string = "rows"`, `class: string = ""`
- Slots: `default`
- Outputs: `sort({ key: string; direction: string })`, `search({ query: string })`, `pageChange({ page: number; pageSize: number })`, `select({ selected: Array<string | number>; all: boolean })`, `change({ page: number; pageSize: number; total: number; query: string; sortKey: string; sortDirection: string })`, `rowClick({ row: object; sourceEvent: Event })`, `action({ id: string; selected: Array<string | number>; rows: object[]; sourceEvent: Event })`, `request({ instanceId: number; page: number; pageSize: number; sortKey: string; sortDirection: string; query: string })`
-220
View File
@@ -1,220 +0,0 @@
# @wrnexus/ui
> 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.
## Overview
`@wrnexus/ui` ships a library of server-rendered `.wrn` components (layout, form
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(--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.
The complete PDF-aligned catalog currently contains **85 components**. The
generated `COMPONENTS.md` and `component-reference.json` files document every
mount name, prop, inferred type, default/required status, slot, event, category,
and source file directly from the packaged `.wrn` source.
## Installation
```bash
bun add @wrnexus/ui
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (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 wrn it into the router for you
(see [Auto-discovery](#auto-discovery)).
## Components
Components live as `.wrn` files under `packages/ui/components/`. The canonical mount
name comes from the component declaration (for example, `component Button` mounts as
`data-component="Button"`). Component lookup is case-insensitive, so existing lowercase
mounts continue to work. Each component accepts a `class` prop (appended to its root
element), and most render their body from either a named prop or the default slot.
### Layout
| Name | Purpose | Key props |
| ----------- | ---------------------------------- | ----------- |
| `container` | Max-width centered content wrapper | `class` |
| `stack` | Vertical column with gap | `gap` (08) |
| `hstack` | Horizontal row with gap | `gap` (08) |
| `grid` | CSS grid container | see source |
| `divider` | Horizontal rule | `class` |
| `spacer` | Flexible/empty spacing element | see source |
### 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-wrn-theme-toggle`) | `label` |
### Additional controls & data display
Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`,
`tooltip`, `table`, `FAQAccordion`, `AnnouncementBar`, and `BackToTop`.
The PDF-defined minimum release and essential build-first set also includes
typed typography, form primitives, loading actions, combobox and multi-select,
time/date-time and recurring schedule controls, confirmation dialogs, data
tables, filters, desktop/mobile navigation, mega menus, marketing/product/legal
page shells, product and metric cards, FAQ composition, pricing comparison,
SDK tabs, legal navigation, and cookie preferences.
`Seo` and `StructuredData` remain framework/page concerns rather than body
components: use the native page `seo { ... }` block and document-head APIs so
metadata is emitted in `<head>` instead of invalid component markup.
The authoritative, always-current list is `uiComponentNames()` (below), which reads
the component directory at runtime.
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
equivalent is exported as `@wrnexus/ui/component-reference.json`.
## API
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
locate the component assets. There is no component code to import — the components
are `.wrn` files rendered server-side.
| Export | Signature | Returns |
| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
| `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 `.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. |
### `./ui.css` asset export
`package.json` also exposes the raw stylesheet as a subpath asset:
```json
"exports": {
".": "./src/index.ts",
"./ui.css": "./ui.css"
}
```
The framework serves this stylesheet once at `/__wrnexus/ui.css`, so pages get all
component styles from a single request.
### Component styles and motion
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 `--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
The active theme and palette are not limited to `@wrnexus/ui` components. The
framework exposes the resolved values as semantic CSS custom properties, so
pages and custom `.wrn` components can use the same contract:
```css
.account-card {
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
border: 1px solid var(--wrn-color-border);
}
.account-card__action {
background: var(--wrn-color-primary);
color: var(--wrn-color-primary-contrast);
}
```
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,
`blue-*` to info, `emerald-*`/`green-*` to success, `amber-*` to warning, and
`red-*`/`rose-*` to danger. These aliases live at `:root`, so they work outside
a `[data-component]` boundary too.
## Usage
### Auto-discovery
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 wrn the UI directory in for you:
```ts
import { buildRouter } from "@wrnexus/router";
import { uiComponentsDir } from "@wrnexus/ui";
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
```
### Mounting components in a page
Once discovered, mount any component by name via `data-component`. Quoted
attributes (other than `data-component`) become string props:
```html
<div data-component="card">
<div data-component="badge" label="New"></div>
<button data-component="button" label="Save" variant="primary" size="lg"></button>
<div data-component="alert" variant="success" title="Done" message="Saved."></div>
</div>
```
## Overrides
Ways to customize the components, in increasing order of power:
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 `.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
the base styles.
4. **`wrnexus eject <name>`** — copy the component's `.wrn` source into your
`app/components`, where (because app components shadow library ones) you fully
own and can edit it. Use `uiComponentNames()` for the list of ejectable names.
## Requirements / Notes
- **Bun-only** — the package uses standard fs/path/url APIs but is published and
consumed within the Bun-native WrNexus toolchain (Node is not supported).
- Peer packages: components are discovered and rendered by
[`@wrnexus/router`](../router) (via `componentDirs`) and served by
[`@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-wrn-theme-toggle` attribute — no per-component JS is required.
-515
View File
@@ -1,515 +0,0 @@
{
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
"components": [
{
"name": "Accordion",
"category": "base",
"purpose": "Theme-aware, responsive accordion component."
},
{
"name": "AdvancedSelect",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive advanced select component."
},
{
"name": "Alert",
"category": "base",
"purpose": "Theme-aware, responsive alert component."
},
{
"name": "AnnouncementBar",
"category": "marketing",
"purpose": "Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls."
},
{
"name": "AuthForm",
"category": "core",
"purpose": "Reusable auth form component."
},
{
"name": "AuthSplitLayout",
"category": "core",
"purpose": "Reusable auth split layout component."
},
{
"name": "Avatar",
"category": "base",
"purpose": "Theme-aware, responsive avatar component."
},
{
"name": "AvatarGroup",
"category": "base",
"purpose": "Theme-aware, responsive avatar group component."
},
{
"name": "BackToTop",
"category": "navigation",
"purpose": "Provide a responsive floating control that returns long pages to the top and can show scroll progress."
},
{
"name": "Badge",
"category": "base",
"purpose": "Theme-aware, responsive badge component."
},
{
"name": "Blockquote",
"category": "base",
"purpose": "Theme-aware, responsive blockquote component."
},
{
"name": "Breadcrumb",
"category": "navigation",
"purpose": "Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events."
},
{
"name": "Button",
"category": "base",
"purpose": "Theme-aware, responsive button component."
},
{
"name": "ButtonGroup",
"category": "base",
"purpose": "Theme-aware, responsive button group component."
},
{
"name": "CTASection",
"category": "marketing",
"purpose": "Close a page or major section with conversion-focused copy, actions, and optional supporting visual content."
},
{
"name": "Card",
"category": "base",
"purpose": "Group related content in a responsive themed surface with title, description, content, and supporting slots."
},
{
"name": "Carousel",
"category": "base",
"purpose": "Theme-aware, responsive carousel component."
},
{
"name": "Chart",
"category": "integrations",
"purpose": "Theme-aware, responsive chart component."
},
{
"name": "ChatBubble",
"category": "base",
"purpose": "Theme-aware, responsive chat bubble component."
},
{
"name": "Checkbox",
"category": "forms",
"purpose": "Theme-aware, responsive checkbox component."
},
{
"name": "Clipboard",
"category": "integrations",
"purpose": "Theme-aware, responsive clipboard component."
},
{
"name": "Collapse",
"category": "base",
"purpose": "Theme-aware, responsive collapse component."
},
{
"name": "ColorPicker",
"category": "forms",
"purpose": "Theme-aware, responsive color picker component."
},
{
"name": "Columns",
"category": "layout",
"purpose": "Create responsive balanced content columns with configurable count, gap, density, and maximum width."
},
{
"name": "ComboBox",
"category": "advanced-forms",
"purpose": "Editable autocomplete combobox with local and remote suggestions."
},
{
"name": "Confetti",
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component."
},
{
"name": "Container",
"category": "layout",
"purpose": "Constrain and align page content with responsive gutters and compact, wide, or full width options."
},
{
"name": "ContextMenu",
"category": "overlays",
"purpose": "Open an accessible keyboard-aware action menu from pointer or keyboard context interactions."
},
{
"name": "CustomScrollbar",
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component."
},
{
"name": "DataMap",
"category": "integrations",
"purpose": "Theme-aware, responsive data map component."
},
{
"name": "DataTable",
"category": "tables",
"purpose": "Sortable, filterable, paginated data table with row selection."
},
{
"name": "DatePicker",
"category": "base",
"purpose": "Theme-aware, responsive date picker component."
},
{
"name": "DeviceFrame",
"category": "base",
"purpose": "Theme-aware, responsive device frame component."
},
{
"name": "Divider",
"category": "layout",
"purpose": "Separate related horizontal or vertical content with optional labels, sizes, and semantic colors."
},
{
"name": "DragAndDrop",
"category": "integrations",
"purpose": "Theme-aware, responsive drag and drop component."
},
{
"name": "Drawer",
"category": "overlays",
"purpose": "Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events."
},
{
"name": "Dropdown",
"category": "overlays",
"purpose": "Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement."
},
{
"name": "FeatureCard",
"category": "marketing",
"purpose": "Present one linked feature or service with media, icon, badge, description, and action."
},
{
"name": "FeatureGrid",
"category": "layout",
"purpose": "Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid."
},
{
"name": "FeatureIconCard",
"category": "marketing",
"purpose": "Present a compact feature or benefit with a styled icon, title, description, badge, and optional link."
},
{
"name": "FileInput",
"category": "forms",
"purpose": "Theme-aware, responsive file input component."
},
{
"name": "FileUploadProgress",
"category": "base",
"purpose": "Theme-aware, responsive file upload progress component."
},
{
"name": "Footer",
"category": "layout",
"purpose": "Render structured responsive footer navigation, pre and post content, copyright content, links, and public events."
},
{
"name": "Grid",
"category": "layout",
"purpose": "Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width."
},
{
"name": "Hero",
"category": "marketing",
"purpose": "Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel."
},
{
"name": "HeroActions",
"category": "marketing",
"purpose": "Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking."
},
{
"name": "Image",
"category": "layout",
"purpose": "Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment."
},
{
"name": "Input",
"category": "forms",
"purpose": "Theme-aware, responsive input component."
},
{
"name": "InputGroup",
"category": "forms",
"purpose": "Theme-aware, responsive input group component."
},
{
"name": "InputNumber",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive input number component."
},
{
"name": "Kbd",
"category": "layout",
"purpose": "Theme-aware, responsive kbd component."
},
{
"name": "LayoutSplitter",
"category": "layout",
"purpose": "Theme-aware, responsive layout splitter component."
},
{
"name": "LegendIndicator",
"category": "base",
"purpose": "Theme-aware, responsive legend indicator component."
},
{
"name": "Link",
"category": "layout",
"purpose": "Render an accessible internal or external link with target, relation, size, color, and public focus or click events."
},
{
"name": "List",
"category": "base",
"purpose": "Present structured responsive linked or status items with icons, descriptions, actions, and selection events."
},
{
"name": "ListGroup",
"category": "base",
"purpose": "Theme-aware, responsive list group component."
},
{
"name": "Map",
"category": "integrations",
"purpose": "Present responsive location information and markers with map-ready metadata and movement or marker events."
},
{
"name": "MarketingSectionHeader",
"category": "marketing",
"purpose": "Introduce marketing content with an eyebrow, title, description, and optional linked action."
},
{
"name": "Marquee",
"category": "base",
"purpose": "Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior."
},
{
"name": "MegaMenu",
"category": "navigation",
"purpose": "Theme-aware, responsive mega menu component."
},
{
"name": "MetricCard",
"category": "data",
"purpose": "Display one operational metric with value, suffix, description, icon, trend, progress, and optional action."
},
{
"name": "MetricGrid",
"category": "data",
"purpose": "Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid."
},
{
"name": "Modal",
"category": "overlays",
"purpose": "Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing."
},
{
"name": "Nav",
"category": "navigation",
"purpose": "Theme-aware, responsive nav component."
},
{
"name": "Navbar",
"category": "navigation",
"purpose": "Theme-aware, responsive navbar component."
},
{
"name": "PageHeader",
"category": "marketing",
"purpose": "Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions."
},
{
"name": "Pagination",
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component."
},
{
"name": "PinInput",
"category": "advanced-forms",
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support."
},
{
"name": "Popover",
"category": "overlays",
"purpose": "Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events."
},
{
"name": "PortalDashboard",
"category": "core",
"purpose": "Reusable portal dashboard component."
},
{
"name": "PreferenceSwitcher",
"category": "core",
"purpose": "Reusable preference switcher component."
},
{
"name": "Progress",
"category": "base",
"purpose": "Theme-aware, responsive progress component."
},
{
"name": "PublicPageShell",
"category": "layout",
"purpose": "Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages."
},
{
"name": "Radio",
"category": "forms",
"purpose": "Theme-aware, responsive radio component."
},
{
"name": "RangeSlider",
"category": "forms",
"purpose": "Theme-aware, responsive range slider component."
},
{
"name": "Rating",
"category": "base",
"purpose": "Theme-aware, responsive rating component."
},
{
"name": "Scrollspy",
"category": "navigation",
"purpose": "Theme-aware, responsive scrollspy component."
},
{
"name": "SearchBox",
"category": "forms",
"purpose": "Provide an accessible responsive search field with labels, validation states, sizes, and input or change events."
},
{
"name": "Section",
"category": "layout",
"purpose": "Create a responsive themed page section with controlled spacing, width, borders, and surface treatment."
},
{
"name": "SectionHeader",
"category": "layout",
"purpose": "Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy."
},
{
"name": "Select",
"category": "forms",
"purpose": "Theme-aware, responsive select component."
},
{
"name": "Sidebar",
"category": "navigation",
"purpose": "Theme-aware, responsive sidebar component."
},
{
"name": "Skeleton",
"category": "base",
"purpose": "Theme-aware, responsive skeleton component."
},
{
"name": "Spinner",
"category": "base",
"purpose": "Theme-aware, responsive spinner component."
},
{
"name": "SplitHero",
"category": "marketing",
"purpose": "Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel."
},
{
"name": "StatsBar",
"category": "data",
"purpose": "Present a compact responsive strip of key facts, counts, performance indicators, or trust signals."
},
{
"name": "Stepper",
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component."
},
{
"name": "StrongPassword",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive strong password component."
},
{
"name": "StyledIcon",
"category": "base",
"purpose": "Theme-aware, responsive styled icon component."
},
{
"name": "Switch",
"category": "forms",
"purpose": "Theme-aware, responsive switch component."
},
{
"name": "Tabs",
"category": "navigation",
"purpose": "Switch between related responsive content panels with horizontal or vertical orientation and selection events."
},
{
"name": "TextLink",
"category": "marketing",
"purpose": "Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling."
},
{
"name": "Textarea",
"category": "forms",
"purpose": "Theme-aware, responsive textarea component."
},
{
"name": "TimePicker",
"category": "forms",
"purpose": "Theme-aware, responsive time picker component."
},
{
"name": "Timeline",
"category": "base",
"purpose": "Present responsive chronological activity, milestones, or workflow status with rich item metadata."
},
{
"name": "Toaster",
"category": "core",
"purpose": "Reusable toaster component."
},
{
"name": "ToggleCount",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive toggle count component."
},
{
"name": "TogglePassword",
"category": "advanced-forms",
"purpose": "Accessible password field with optional show and hide controls."
},
{
"name": "Tooltip",
"category": "overlays",
"purpose": "Show concise accessible contextual help on hover, focus, click, or controlled open state."
},
{
"name": "TreeView",
"category": "base",
"purpose": "Theme-aware, responsive tree view component."
},
{
"name": "Typography",
"category": "layout",
"purpose": "Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy."
},
{
"name": "WysiwygEditor",
"category": "integrations",
"purpose": "Theme-aware, responsive wysiwyg editor component."
}
]
}
-12
View File
@@ -1,12 +0,0 @@
{
"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
-478
View File
@@ -1,478 +0,0 @@
component Accordion {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
variant: string = "default"
class: string = ""
id: string = "accordion"
items: unknown[] = []
defaultOpen: unknown[] = []
multiple: boolean = false
alwaysOpen: boolean = false
disabled: boolean = false
indicator: string = "plus"
indicatorPosition: string = "start"
showIndicator: boolean = true
bordered: boolean = false
separated: boolean = false
flush: boolean = false
contentItalic: boolean = false
}
state openValues = defaultOpen
functions {
shared function itemValue(item, index) {
return item.value !== undefined && item.value !== ""
? String(item.value)
: String(index)
}
shared function nestedValue(parent, parentIndex, item, index) {
return itemValue(parent, parentIndex) + "." + itemValue(item, index)
}
shared function isOpen(value) {
return openValues.includes(value)
}
shared function allowsMultiple() {
return multiple || alwaysOpen
}
// 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
}
if (eventName === "open") {
output.open(payload)
} else if (eventName === "close") {
output.close(payload)
} else {
output.change(payload)
}
}
client function toggleItem(sourceEvent, value, item, wasOpen) {
if (disabled || item.disabled) {
return
}
wasOpen = isOpen(value)
if (wasOpen) {
openValues = openValues.filter((entry) => entry !== value)
} else if (allowsMultiple()) {
openValues = openValues.concat([value])
} else {
openValues = [value]
}
dispatchAccordionEvent(
sourceEvent,
wasOpen ? "close" : "open",
value,
item
)
dispatchAccordionEvent(sourceEvent, "change", value, item)
}
}
view {
<div
{...attrs}
id="{id}"
data-wrn-accordion
data-variant="{variant}"
data-indicator="{indicator}"
data-multiple="{allowsMultiple() ? 'true' : 'false'}"
class="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="wrn-next__accordion-item"
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
>
<h3 class="wrn-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}"
aria-expanded="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}"
disabled="{disabled || item.disabled}"
@click="toggleItem(event, itemValue(item, index), item)"
>
{#if showIndicator && indicatorPosition === "start"}
<span class="wrn-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{item.label || item.title}</span>
{#if showIndicator && indicatorPosition === "end"}
<span class="wrn-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
</button>
</h3>
<div
id="{id}-panel-{index}"
class="wrn-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}"
aria-hidden="{isOpen(itemValue(item, index)) ? 'false' : 'true'}"
>
<div>
<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="wrn-next__accordion-nested">
{#each item.children as child, childIndex}
<section
class="wrn-next__accordion-item"
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
>
<h4 class="wrn-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}-{childIndex}"
aria-expanded="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}-{childIndex}"
disabled="{disabled || child.disabled}"
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
>
{#if showIndicator}
<span class="wrn-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{child.label || child.title}</span>
</button>
</h4>
<div
id="{id}-panel-{index}-{childIndex}"
class="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="wrn-next__accordion-content {contentItalic ? 'wrn-next__accordion-content-italic' : ''}">
<p>{child.content}</p>
</div>
</div>
</div>
</section>
{/each}
</div>
{/if}
</div>
</div>
</div>
</section>
{/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;
}
}
}
-588
View File
@@ -1,588 +0,0 @@
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)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Advanced Select"
name: string = ""
value: string = ""
values: unknown[] = []
options: unknown[] = []
groups: unknown[] = []
placeholder: string = "Select an option"
placeholderIcon: string = ""
searchPlaceholder: string = "Search options…"
multiple: boolean = false
searchable: boolean = true
defaultOpen: boolean = false
clearable: boolean = true
allowEmpty: boolean = true
tags: boolean = false
disabled: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
helpText: string = ""
loading: boolean = false
loadingLabel: string = "Loading options…"
emptyLabel: string = "No options found"
selectedOptionsLabel: string = "Selected options"
clearLabel: string = "Clear selection"
createLabel: string = "Create"
loadMoreLabel: string = "Load more"
searchMode: string = "contains"
searchFields: string = "label,description"
minSearchLength: number = 0
searchResultLimit: number = 0
maxSelections: number = 0
showCounter: boolean = false
counterTemplate: string = "{selected} selected"
optionTemplate: string = "default"
selectedTemplate: string = "default"
closeOnSelect: boolean = true
scrollToSelected: boolean = true
fixed: boolean = false
placement: string = "bottom"
remote: boolean = false
remoteUrl: string = ""
remoteQueryParam: string = "q"
remoteDebounce: number = 250
remoteAutoLoad: boolean = true
infinite: boolean = false
hasMore: boolean = false
page: number = 1
class: string = ""
}
state open = defaultOpen
state query: string = ""
state activeIndex: number = -1
state selectedValue = value
state selectedValues = values
functions {
shared function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
shared function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
shared function matches(option) {
if (!query || query.length < Number(minSearchLength || 0)) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
shared function matchingOptions(list) {
return list.filter((option) => matches(option))
}
shared function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, Number(searchResultLimit))
}
return matchingOptions(list)
}
shared function flatVisibleOptions() {
return visibleOptions(allOptions())
}
shared function isSelected(option) {
return (
multiple
? selectedValues.includes(option.value)
: selectedValue === option.value
)
}
shared function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
shared function selectedCount() {
return multiple ? selectedValues.length : selectedValue ? 1 : 0
}
shared function counterText() {
return (
maxSelections
? selectedCount() + " / " + maxSelections + " selected"
: selectedCount() + " selected"
)
}
shared function selectedText() {
return (
multiple
? selectedValues.join(", ")
: selectedOptions().length
? selectedOptions()[0].label
: ""
)
}
shared function triggerText() {
return selectedCount() ? selectedText() : placeholder
}
shared function canSelect(option) {
if (option.disabled) {
return false
}
if (!multiple || isSelected(option)) {
return true
}
if (maxSelections <= 0) {
return true
}
return selectedCount() < maxSelections
}
shared function chooseSingle(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = ""
if (closeOnSelect) {
open = false
}
}
shared function chooseMultiple(option) {
if (!canSelect(option)) {
return
}
if (isSelected(option)) {
selectedValues = selectedValues.filter((item) => item !== option.value)
} else {
selectedValues = selectedValues.concat([option.value])
}
query = ""
}
shared function chooseOption(option) {
if (multiple) {
chooseMultiple(option)
return
}
chooseSingle(option)
}
client function clearSelection(event) {
event.stopPropagation()
selectedValue = ""
selectedValues = []
query = ""
open = false
}
shared function toggle() {
if (disabled) {
return;
};
open = !open;
activeIndex = open && flatVisibleOptions().length ? 0 : -1;
}
shared function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
client function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(-1)
} else if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
if (!open) {
open = true
} else if (activeIndex >= 0) {
chooseOption(flatVisibleOptions()[activeIndex])
}
} else if (event.key === "Escape") {
open = false
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
shared function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="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'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
>
<div class="wrn-next__row">
<label id="{name}-label" for="{name}-trigger">{label}</label>
<small data-show="showCounter" data-text="counterText()">{counterText()}</small>
</div>
<div class="wrn-next__select-control">
<button
{...attrs}
id="{name}-trigger"
type="button"
class="wrn-next__select-trigger"
role="combobox"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
@click="toggle()"
@keydown="handleKeydown(event)"
>
<span class="wrn-next__select-value">
<span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
<span
data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
data-text="selectedCount() + ' selected'"
>{selectedCount()} selected</span>
<span
data-show="multiple && selectedTemplate === 'text' && selectedCount() > 0"
data-text="selectedText()"
>{selectedText()}</span>
<span
class="wrn-next__select-tags"
aria-label="{selectedOptionsLabel}"
data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
>
{#each allOptions() as option}
<span data-show="isSelected(option)">
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="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="wrn-next__placeholder">{placeholder}</span>
</span>
</span>
<span
class="wrn-next__select-chevron icon-[lucide--chevrons-up-down]"
aria-hidden="true"
></span>
</button>
<button
type="button"
class="wrn-next__clear-select"
data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearSelection(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
</div>
<div
class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
data-show="open"
style="{open ? '' : 'display: none'}"
>
{#if searchable}
<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}"
placeholder="{searchPlaceholder}"
autocomplete="off"
@input="query = event.target.value; activeIndex = flatVisibleOptions().length ? 0 : -1"
@keydown="handleKeydown(event)"
/>
</label>
{/if}
<div
class="wrn-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<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="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="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="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"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="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="wrn-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="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"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="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="wrn-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
{#if !loading && !flatVisibleOptions().length}
<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="wrn-next__create-option">{createLabel} “{query}”</button>
{/if}
<button
type="button"
class="wrn-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
<input
type="hidden"
name="{name}"
value="{multiple ? selectedValues.join(',') : selectedValue}"
required="{required}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="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;
}
}
}
-425
View File
@@ -1,425 +0,0 @@
component AvatarGroup {
outputs {
overflow(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
items: unknown[] = []
size: string = "md"
color: string = "primary"
variant: string = "solid"
shape: string = "circle"
layout: string = "stack"
maxVisible: number = 4
columns: number = 3
borderColor: string = ""
showTooltips: boolean = true
overflowLabel: string = "Show remaining members"
class: string = ""
}
state overflowOpen: boolean = false
functions {
shared function visibleMembers() {
return items.slice(0, Number(maxVisible))
}
shared function hiddenMembers() {
return items.slice(Number(maxVisible))
}
client function toggleOverflow() {
overflowOpen = !overflowOpen
output.overflow({
component: "AvatarGroup",
open: overflowOpen,
hiddenCount: hiddenMembers().length
})
}
}
view {
<div
{...attrs}
class="wrn-component wrn-next--avatar-group {class}"
data-wrn-avatar-group
data-layout="{layout}"
data-shape="{shape}"
data-size="{size}"
style="--wrn-avatar-group-columns:{columns}; --wrn-avatar-group-ring:{borderColor || 'var(--wrn-color-bg)'}"
role="group"
aria-label="Avatar group"
>
<div
class="wrn-next__avatar-group-members"
>
{#each visibleMembers() as item}
<span
class="wrn-next__avatar-group-member"
data-size="{item.size || size}"
data-shape="{item.shape || shape}"
data-color="{item.color || color}"
data-variant="{item.variant || variant}"
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
>
<span
class="wrn-next__avatar-group-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt="{item.alt || item.name || ''}"
loading="lazy"
/>
{:else if item.initials}
<span
aria-hidden="true"
>
{item.initials}
</span>
{:else}
<span
class="icon-[lucide--user-round]"
aria-hidden="true"
>
</span>
{/if}
</span>
{#if showTooltips && (item.tooltip || item.name)}
<span
class="wrn-next__avatar-group-tooltip"
role="tooltip"
>
{item.tooltip || item.name}
</span>
{/if}
</span>
{/each}
{#if hiddenMembers().length > 0}
<span
class="wrn-next__avatar-group-overflow"
>
<button
type="button"
class="wrn-next__avatar-group-overflow-button"
aria-label="{overflowLabel}"
aria-expanded="{overflowOpen ? 'true' : 'false'}"
@click="toggleOverflow(event)"
>
+{hiddenMembers().length}
</button>
<span
class="wrn-next__avatar-group-menu"
role="menu"
data-show="overflowOpen"
aria-hidden="{overflowOpen ? 'false' : 'true'}"
>
{#each hiddenMembers() as item}
<span
class="wrn-next__avatar-group-menu-item"
role="menuitem"
>
<span
class="wrn-next__avatar-group-menu-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt=""
loading="lazy"
/>
{:else}
<span>{item.initials || '?'}</span>
{/if}
</span>
<span>{item.name || item.alt || item.initials || 'Team member'}</span>
</span>
{/each}
</span>
</span>
{/if}
</div>
</div>
}
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;
}
}
}
-339
View File
@@ -1,339 +0,0 @@
component Blockquote {
props {
quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."
citation: string = ""
citationTitle: string = ""
citationUrl: string = ""
avatarSrc: string = ""
avatarAlt: string = ""
size: string = "md"
color: string = "primary"
align: string = "left"
variant: string = "default"
quoteMark: boolean = true
italic: boolean = true
class: string = ""
}
view {
<figure
{...attrs}
class="wrn-component wrn-next--blockquote wrn-component--color-{color} wrn-component--size-{size} {class}"
data-size="{size}"
data-color="{color}"
data-align="{align}"
data-variant="{variant}"
data-italic="{italic}"
>
<blockquote cite="{citationUrl}">
{#if quoteMark}
<span class="wrn-next__blockquote-mark" aria-hidden="true">“</span>
{/if}
<div class="wrn-next__blockquote-copy">
{#if quote}
<p>{quote}</p>
{:else}
<slot />
{/if}
</div>
</blockquote>
{#if citation || citationTitle || avatarSrc}
<figcaption class="wrn-next__blockquote-citation">
{#if avatarSrc}
<img
class="wrn-next__blockquote-avatar"
src="{avatarSrc}"
alt="{avatarAlt}"
loading="lazy"
/>
{/if}
<span class="wrn-next__blockquote-attribution">
{#if citation}
{#if citationUrl}
<cite><a href="{citationUrl}">{citation}</a></cite>
{:else}
<cite>{citation}</cite>
{/if}
{/if}
{#if citationTitle}<span>{citationTitle}</span>{/if}
</span>
</figcaption>
{/if}
</figure>
}
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;
}
}
}
-323
View File
@@ -1,323 +0,0 @@
component Breadcrumb {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
label: string = "Breadcrumb"
items: unknown[] = []
active: string = ""
separator: string = "chevron"
showHome: boolean = false
homeLabel: string = "Home"
homeHref: string = "/"
homeIcon: string = "icon-[lucide--house]"
size: string = "default"
color: string = "primary"
variant: string = "minimal"
class: string = ""
}
view {
<nav
{...attrs}
data-ui-component="Breadcrumb"
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
aria-label='{label}'
class='wrn-breadcrumb {class}'
>
<ol class="wrn-breadcrumb__list">
{#if showHome}
<li class="wrn-breadcrumb__item wrn-breadcrumb__item--home">
<a
href='{homeHref || "/"}'
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] wrn-breadcrumb__icon" aria-hidden="true"></span>
{:else if homeIcon}
<span class='{homeIcon} wrn-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wrn-breadcrumb__home-label">{homeLabel}</span>
</a>
</li>
{/if}
{#each items as item, itemIndex}
<li
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="wrn-breadcrumb__separator" aria-hidden="true">
{#if separator === "slash"}
<span>/</span>
{:else if separator === "dot"}
<span>•</span>
{:else if separator === "arrow"}
<span>→</span>
{:else}
<span class="icon-[lucide--chevron-right] wrn-breadcrumb__separator-icon"></span>
{/if}
</span>
{/if}
{#if item.href && !item.disabled && !(item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1))}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
class="wrn-breadcrumb__link"
@click='output.select({ item: item, itemIndex: itemIndex })'
>
{#if item.icon}
<span class='{item.icon} wrn-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wrn-breadcrumb__label">{item.label || item.title}</span>
{#if item.external}
<span class="icon-[lucide--arrow-up-right] wrn-breadcrumb__external" aria-hidden="true"></span>
{/if}
</a>
{:else}
<span
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} wrn-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wrn-breadcrumb__label">{item.label || item.title}</span>
</span>
{/if}
</li>
{/each}
</ol>
</nav>
}
style {
.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(--wrn-color-text-muted);
}
.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);
}
.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);
}
.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);
}
.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);
}
.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);
}
.wrn-breadcrumb[data-variant="soft"] {
width: fit-content;
max-width: 100%;
padding: 0.45rem 0.65rem;
background: var(--breadcrumb-soft);
border: 1px solid var(--breadcrumb-muted);
border-radius: 0.75rem;
}
.wrn-breadcrumb[data-variant="outline"] {
width: fit-content;
max-width: 100%;
padding: 0.45rem 0.65rem;
background: var(--wrn-color-surface-raised);
border: 1px solid var(--wrn-color-border);
border-radius: 0.75rem;
box-shadow: var(--wrn-shadow-sm);
}
.wrn-breadcrumb[data-variant="contrast"] {
color: rgba(255, 255, 255, 0.78);
}
.wrn-breadcrumb__list {
display: flex;
align-items: center;
gap: 0;
min-width: 0;
margin: 0;
padding: 0;
overflow-x: auto;
list-style: none;
scrollbar-width: none;
white-space: nowrap;
}
.wrn-breadcrumb__list::-webkit-scrollbar {
display: none;
}
.wrn-breadcrumb__item {
display: inline-flex;
align-items: center;
min-width: 0;
flex: 0 0 auto;
}
.wrn-breadcrumb__separator {
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline: 0.45rem;
color: var(--wrn-color-text-subtle);
font-size: 0.75rem;
opacity: 0.72;
}
.wrn-breadcrumb__separator-icon {
width: 0.9rem;
height: 0.9rem;
}
.wrn-breadcrumb__link,
.wrn-breadcrumb__current {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
min-height: 2rem;
padding: 0.25rem 0.4rem;
border-radius: 0.55rem;
font-size: 0.8125rem;
line-height: 1.25rem;
text-decoration: none;
}
.wrn-breadcrumb[data-size="sm"] .wrn-breadcrumb__link,
.wrn-breadcrumb[data-size="sm"] .wrn-breadcrumb__current {
min-height: 1.75rem;
font-size: 0.75rem;
}
.wrn-breadcrumb[data-size="lg"] .wrn-breadcrumb__link,
.wrn-breadcrumb[data-size="lg"] .wrn-breadcrumb__current {
min-height: 2.25rem;
font-size: 0.875rem;
}
.wrn-breadcrumb__link {
color: var(--wrn-color-text-muted);
font-weight: 600;
transition:
color 160ms ease,
background 160ms ease;
}
.wrn-breadcrumb__link:hover {
color: var(--breadcrumb-accent);
background: var(--breadcrumb-soft);
}
.wrn-breadcrumb__link:focus-visible {
color: var(--breadcrumb-accent);
outline: 2px solid var(--breadcrumb-accent);
outline-offset: 2px;
}
.wrn-breadcrumb__current {
max-width: 22rem;
color: var(--wrn-color-text);
font-weight: 750;
}
.wrn-breadcrumb__item[data-current="true"] .wrn-breadcrumb__current {
color: var(--breadcrumb-text);
background: var(--breadcrumb-soft);
}
.wrn-breadcrumb__current[aria-disabled="true"] {
opacity: 0.58;
}
.wrn-breadcrumb__icon,
.wrn-breadcrumb__external {
flex: 0 0 auto;
width: 0.95rem;
height: 0.95rem;
}
.wrn-breadcrumb__home {
color: var(--breadcrumb-accent);
}
.wrn-breadcrumb__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.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;
}
.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);
}
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__separator {
color: rgba(255, 255, 255, 0.62);
}
@media (max-width: 639px) {
.wrn-breadcrumb__home-label {
display: none;
}
.wrn-breadcrumb__current {
max-width: 13rem;
}
}
@media (prefers-reduced-motion: reduce) {
.wrn-breadcrumb__link {
transition: none;
}
}
}
}
-304
View File
@@ -1,304 +0,0 @@
component ButtonGroup {
outputs {
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
change(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
}
props {
items: unknown[] = []
value: string = ""
size: string = "md"
color: string = "primary"
variant: string = "default"
orientation: string = "horizontal"
responsive: boolean = false
attached: boolean = true
selectable: boolean = false
toolbar: boolean = false
disabled: boolean = false
ariaLabel: string = "Button group"
class: string = ""
}
state selectedValue = value
functions {
shared function itemValue(item, index) {
return item.value || item.label || String(index)
}
client function selectItem(item, index) {
previousValue = selectedValue
nextValue = itemValue(item, index)
output.select({
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
if (selectable && previousValue !== nextValue) {
selectedValue = nextValue
output.change({
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
}
}
}
view {
<div
{...attrs}
class="wrn-component wrn-next--button-group {class}"
role="{toolbar ? 'toolbar' : 'group'}"
aria-label="{ariaLabel}"
aria-orientation="{orientation}"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-orientation="{orientation}"
data-responsive="{responsive}"
data-attached="{attached}"
data-selectable="{selectable}"
data-disabled="{disabled}"
>
{#if items}
{#each items as item, index}
<button
type="{item.type || 'button'}"
class="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') : ''}"
data-value="{item.value || item.label || String(index)}"
data-selected="{selectable && selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false'}"
@click="selectItem(item, index)"
>
{#if item.icon}
<span class="wrn-btn__icon {item.icon}" aria-hidden="true"></span>
{/if}
<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;
}
}
}
-898
View File
@@ -1,898 +0,0 @@
// Interactive, hydration-safe content carousel.
component Carousel {
outputs {
initialize(payload: { index: number; count: number })
change(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })
previous(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
next(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
play(payload: { index: number; interval: number })
pause(payload: { index: number })
reachStart(payload: { index: number })
reachEnd(payload: { index: number })
dragStart(payload: { index: number; x: null })
dragEnd(payload: { index: number; distance: number })
}
props {
size: string = "default"
color: string = "primary"
title: string = ""
description: string = ""
items: unknown[] = []
activeIndex: number = 0
slidesPerView: number = 1
gap: string = "0.75rem"
showPagination: boolean = false
isAutoPlay: boolean = false
autoplayInterval: number = 4000
isInfiniteLoop: boolean = false
isRTL: boolean = false
isCentered: boolean = false
isDraggable: boolean = false
isAutoHeight: boolean = false
isSnap: boolean = false
showCounter: boolean = false
thumbnails: string = "none"
ariaLabel: string = "Content carousel"
variant: string = "default"
class: string = ""
}
state currentIndex = activeIndex
state playing: boolean = false
state dragOrigin = null
state dragOffset: number = 0
state autoplayTimer = null
state carouselRoot = null
functions {
shared function slideCount() {
return items.length
}
shared function maximumIndex(count, visible) {
count = slideCount()
visible = Math.max(1, Number(slidesPerView) || 1)
if (isCentered || isSnap) {
return Math.max(0, count - 1)
}
return Math.max(0, count - visible)
}
shared function normalizedIndex(index, maximum) {
maximum = maximumIndex()
if (slideCount() === 0) {
return 0
}
if (isInfiniteLoop) {
if (index < 0) {
return maximum
}
if (index > maximum) {
return 0
}
}
return Math.max(0, Math.min(index, maximum))
}
client function rememberRoot(sourceEvent, root) {
if (!sourceEvent || !sourceEvent.currentTarget) {
return
}
root = sourceEvent.currentTarget.closest(".wrn-next--carousel")
if (root) {
carouselRoot = root
}
}
client function syncNavigation(sourceEvent, index, root, viewport, slides, slide, thumbnails, thumbnail, rail) {
rememberRoot(sourceEvent)
root = carouselRoot
if (!root) {
return
}
if (isSnap) {
viewport = root.querySelector(".wrn-next__carousel-viewport")
slides = root.querySelectorAll(".wrn-next__carousel-slide")
slide = slides[index]
if (viewport && slide) {
viewport.scrollTo({
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
behavior: "smooth"
})
}
}
thumbnails = root.querySelectorAll(".wrn-next__carousel-thumbnails button")
if (thumbnails[index]) {
thumbnail = thumbnails[index]
rail = thumbnail.parentElement
rail.scrollTo({
left: thumbnail.offsetLeft - (rail.clientWidth - thumbnail.clientWidth) / 2,
top: thumbnail.offsetTop - (rail.clientHeight - thumbnail.clientHeight) / 2,
behavior: "smooth"
})
}
}
client function handleSnapScroll(sourceEvent, slides, viewport, viewportCenter, closestIndex, closestDistance, slide, slideCenter, distance, previousIndex) {
if (!isSnap) {
return
}
rememberRoot(sourceEvent)
viewport = sourceEvent.currentTarget
slides = viewport.querySelectorAll(".wrn-next__carousel-slide")
viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2
closestIndex = currentIndex
closestDistance = 1000000000
slides.forEach(function (candidate, index) {
slideCenter = candidate.offsetLeft + candidate.clientWidth / 2
distance = Math.abs(slideCenter - viewportCenter)
if (distance < closestDistance) {
closestDistance = distance
closestIndex = index
}
})
closestIndex = normalizedIndex(closestIndex)
if (closestIndex !== currentIndex) {
previousIndex = currentIndex
currentIndex = closestIndex
output.change({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex],
reason: "snap"
})
slides = carouselRoot.querySelectorAll(".wrn-next__carousel-thumbnails button")
slide = slides[currentIndex]
if (slide) {
viewport = slide.parentElement
viewport.scrollTo({
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
top: slide.offsetTop - (viewport.clientHeight - slide.clientHeight) / 2,
behavior: "smooth"
})
}
}
}
client function selectSlide(index, reason, sourceEvent, previousIndex) {
if (slideCount() === 0) {
return
}
previousIndex = currentIndex
currentIndex = normalizedIndex(Number(index))
syncNavigation(sourceEvent, currentIndex)
if (previousIndex === currentIndex && reason !== "initialize") {
return
}
output.change({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex],
reason: reason || "select"
})
if (currentIndex === 0) {
output.reachStart({ index: currentIndex })
}
if (currentIndex === maximumIndex()) {
output.reachEnd({ index: currentIndex })
}
}
client function previousSlide(sourceEvent, previousIndex) {
previousIndex = currentIndex
selectSlide(currentIndex - 1, "previous", sourceEvent)
output.previous({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex]
})
}
client function nextSlide(sourceEvent, previousIndex) {
previousIndex = currentIndex
selectSlide(currentIndex + 1, "next", sourceEvent)
output.next({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex]
})
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "ArrowLeft") {
sourceEvent.preventDefault()
if (isRTL) {
nextSlide(sourceEvent)
} else {
previousSlide(sourceEvent)
}
}
if (sourceEvent.key === "ArrowRight") {
sourceEvent.preventDefault()
if (isRTL) {
previousSlide(sourceEvent)
} else {
nextSlide(sourceEvent)
}
}
if (sourceEvent.key === "Home") {
sourceEvent.preventDefault()
selectSlide(0, "keyboard", sourceEvent)
}
if (sourceEvent.key === "End") {
sourceEvent.preventDefault()
selectSlide(maximumIndex(), "keyboard", sourceEvent)
}
}
client function beginDrag(sourceEvent) {
if (!isDraggable || isSnap) {
return
}
sourceEvent.preventDefault()
dragOrigin = sourceEvent.clientX
dragOffset = 0
sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId)
output.dragStart({ index: currentIndex, x: dragOrigin })
}
client function moveDrag(sourceEvent) {
if (!isDraggable || isSnap || dragOrigin === null) {
return
}
sourceEvent.preventDefault()
dragOffset = sourceEvent.clientX - dragOrigin
}
shared function cancelDrag() {
dragOrigin = null
dragOffset = 0
}
client function endDrag(sourceEvent, distance) {
if (!isDraggable || isSnap || dragOrigin === null) {
return
}
sourceEvent.preventDefault()
distance = dragOffset || sourceEvent.clientX - dragOrigin
dragOrigin = null
dragOffset = 0
if (Math.abs(distance) > 20) {
if ((distance < 0 && !isRTL) || (distance > 0 && isRTL)) {
nextSlide(sourceEvent)
} else {
previousSlide(sourceEvent)
}
}
output.dragEnd({
index: currentIndex,
distance: distance
})
}
shared function advanceAutoplay() {
if (currentIndex >= maximumIndex()) {
selectSlide(0, "autoplay")
} else {
selectSlide(currentIndex + 1, "autoplay")
}
}
client function startAutoplay() {
if (!isAutoPlay || playing || slideCount() < 2) {
return
}
playing = true
autoplayTimer = setInterval(advanceAutoplay, Math.max(1000, Number(autoplayInterval)))
output.play({ index: currentIndex, interval: autoplayInterval })
}
client function pauseAutoplay() {
if (autoplayTimer) {
clearInterval(autoplayTimer)
}
autoplayTimer = null
if (playing) {
output.pause({ index: currentIndex })
}
playing = false
}
}
lifecycle {
mount {
output.initialize({ index: currentIndex, count: slideCount() })
startAutoplay()
}
unmount {
if (autoplayTimer) {
clearInterval(autoplayTimer)
}
}
}
view {
<section
{...attrs}
class="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}"
data-dragging="{dragOrigin !== null}"
data-auto-height="{isAutoHeight}"
data-snap="{isSnap}"
data-thumbnails="{thumbnails}"
dir="{isRTL ? 'rtl' : 'ltr'}"
role="region"
aria-roledescription="carousel"
aria-label="{ariaLabel}"
@keydown="handleKeydown(event)"
@mouseenter="rememberRoot(event); pauseAutoplay()"
@mouseleave="startAutoplay()"
@focusin="rememberRoot(event); pauseAutoplay()"
@focusout="startAutoplay()"
>
{#if title || description}
<header class="wrn-next__carousel-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
<div class="wrn-next__carousel-layout">
{#if thumbnails === "vertical"}
<div class="wrn-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}: {item.label || item.title}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'thumbnail', event)"
>
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
</button>
{/each}
</div>
{/if}
<div class="wrn-next__carousel-main">
<div class="wrn-next__carousel-stage">
<div
class="wrn-next__carousel-viewport"
tabindex="0"
@scroll="handleSnapScroll(event)"
@pointerdown="beginDrag(event)"
@pointermove="moveDrag(event)"
@pointerup="endDrag(event)"
@pointercancel="cancelDrag()"
@lostpointercapture="cancelDrag()"
>
<div
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="wrn-next__carousel-slide"
data-active="{index === currentIndex}"
role="group"
aria-roledescription="slide"
aria-label="{index + 1} of {items.length}"
aria-hidden="{index === currentIndex ? 'false' : 'true'}"
>
{#if item.imageSrc}
<img src="{item.imageSrc}" alt="{item.imageAlt || item.title || ''}" />
{/if}
<div class="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}
{#if item.actionLabel}
<a href="{item.actionHref || '#'}">{item.actionLabel}</a>
{/if}
</div>
</article>
{/each}
<slot />
</div>
</div>
<button
class="wrn-next__carousel-control wrn-next__carousel-control--previous"
type="button"
aria-label="Previous slide"
disabled="{!isInfiniteLoop && currentIndex === 0}"
@click="previousSlide(event)"
>
<span class="icon-[lucide--chevron-left]" aria-hidden="true"></span>
</button>
<button
class="wrn-next__carousel-control wrn-next__carousel-control--next"
type="button"
aria-label="Next slide"
disabled="{!isInfiniteLoop && currentIndex === maximumIndex()}"
@click="nextSlide(event)"
>
<span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
</button>
{#if showCounter}
<output class="wrn-next__carousel-counter" aria-live="polite">
{currentIndex + 1} / {items.length}
</output>
{/if}
</div>
{#if showPagination}
<div class="wrn-next__carousel-pagination" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'pagination', event)"
></button>
{/each}
</div>
{/if}
{#if thumbnails === "horizontal"}
<div class="wrn-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}: {item.label || item.title}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'thumbnail', event)"
>
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
</button>
{/each}
</div>
{/if}
</div>
</div>
</section>
}
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;
}
}
}
-107
View File
@@ -1,107 +0,0 @@
component Chart {
outputs {
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 {
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 = ""
}
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 {
<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 />
</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; }
}
}
-418
View File
@@ -1,418 +0,0 @@
component ChatBubble {
outputs {
action(payload: { action: boolean; item: string | number | boolean | null | object; index: number })
messageClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
avatarClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
linkClick(payload: { link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
}
props {
size: string = "default"
color: string = "primary"
title: string = ""
description: string = ""
items: unknown[] = []
oneSided: boolean = false
showAvatars: boolean = false
showMetadata: boolean = false
ariaLabel: string = "Conversation"
variant: string = "default"
class: string = ""
}
functions {
shared function messageDirection(item) {
return item.direction === "outgoing" ? "outgoing" : "incoming"
}
client function selectMessage(item, index) {
output.messageClick({
item: item,
index: index,
direction: messageDirection(item)
})
}
client function selectAvatar(sourceEvent, item, index) {
sourceEvent.stopPropagation()
output.avatarClick({
item: item,
index: index,
direction: messageDirection(item)
})
}
client function selectLink(sourceEvent, link, item, index) {
sourceEvent.stopPropagation()
output.linkClick({
link: link,
item: item,
index: index
})
}
client function selectAction(sourceEvent, item, index) {
sourceEvent.stopPropagation()
output.action({
action: item.action || "retry",
item: item,
index: index
})
}
}
view {
<section
{...attrs}
class="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="wrn-next__chat-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
{#if items.length}
<div class="wrn-next__chat-thread">
{#each items as item, index}
<article
class="wrn-next__chat-message"
data-direction="{messageDirection(item)}"
aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
>
<div class="wrn-next__chat-row">
{#if showAvatars && (item.avatarSrc || item.avatarFallback)}
<button
class="wrn-next__chat-avatar"
type="button"
aria-label="{item.avatarLabel || item.author || 'Message author'}"
@click="selectAvatar(event, item, index)"
>
{#if item.avatarSrc}
<img src="{item.avatarSrc}" alt="{item.avatarAlt || ''}" />
{:else}
<span>{item.avatarFallback}</span>
{/if}
</button>
{/if}
<div
class="wrn-next__chat-content"
role="button"
tabindex="0"
@click="selectMessage(item, index)"
@keydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMessage(item, index) }"
>
{#if item.title}<h4>{item.title}</h4>{/if}
{#if item.text}<p>{item.text}</p>{/if}
{#if item.bullets && item.bullets.length}
<ul>
{#each item.bullets as bullet}<li>{bullet}</li>{/each}
</ul>
{/if}
{#if item.links && item.links.length}
<nav aria-label="Message links">
{#each item.links as link}
<a
href="{link.href || '#'}"
@click="selectLink(event, link, item, index)"
>{link.label}</a>
{/each}
</nav>
{/if}
</div>
</div>
{#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
<footer class="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}
{#if item.actionLabel}
<button type="button" @click="selectAction(event, item, index)">
{item.actionLabel}
</button>
{/if}
</footer>
{/if}
</article>
{/each}
</div>
{/if}
<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;
}
}
}
-97
View File
@@ -1,97 +0,0 @@
component Clipboard {
outputs {
copy(payload: { value: string; sourceEvent?: Event })
success(payload: { value: string; sourceEvent?: Event })
error(payload: { error: Error | string; value: string; sourceEvent?: Event })
}
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 = ""
}
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
{...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; }
}
}
-315
View File
@@ -1,315 +0,0 @@
component Collapse {
outputs {
toggle(payload: { index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })
open(payload: { index: number; item: string | number | boolean | null | object })
close(payload: { index: number; item: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
items: unknown[] = []
multiple: boolean = false
mode: string = "panel"
initialOpenIndexes: unknown[] = []
ariaLabel: string = "Collapsible content"
class: string = ""
}
state openIndexes = initialOpenIndexes
functions {
shared function isOpen(index) {
return openIndexes.includes(index)
}
client function toggleItem(index, item, opening) {
if (item.disabled) {
return
}
opening = !isOpen(index)
if (opening) {
if (multiple) {
openIndexes = openIndexes.concat(index)
} else {
openIndexes = [index]
}
output.open({ index: index, item: item })
} else {
openIndexes = openIndexes.filter((openIndex) => openIndex !== index)
output.close({ index: index, item: item })
}
output.toggle({
index: index,
item: item,
open: opening,
openIndexes: openIndexes
})
}
}
view {
<section
{...attrs}
class="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="wrn-next__collapse-item" data-open="{isOpen(index)}">
{#if mode === "inline" && item.preview}
<p class="wrn-next__collapse-preview">{item.preview}</p>
{/if}
<button
class="wrn-next__collapse-trigger"
type="button"
aria-expanded="{isOpen(index) ? 'true' : 'false'}"
aria-controls="{item.id || 'collapse-panel-' + index}"
disabled="{item.disabled}"
@click="toggleItem(index, item)"
>
<span>{isOpen(index) ? (item.closeLabel || "Read less") : item.label}</span>
<span class="icon-[lucide--chevron-down] wrn-next__collapse-chevron" aria-hidden="true"></span>
</button>
<div
id="{item.id || 'collapse-panel-' + index}"
class="wrn-next__collapse-panel"
data-open="{isOpen(index)}"
aria-hidden="{isOpen(index) ? 'false' : 'true'}"
inert="{!isOpen(index)}"
>
<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}
<nav aria-label="Related links">
{#each item.links as link}<a href="{link.href || '#'}">{link.label}</a>{/each}
</nav>
{/if}
</div>
</div>
</article>
{/each}
<slot />
</section>
}
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;
}
}
}
-607
View File
@@ -1,607 +0,0 @@
component ColorPicker {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Color"
hiddenLabel: boolean = false
placeholder: string = ""
value: string = "#2563eb"
icon: string = ""
iconPosition: string = "start"
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
functions {
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="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);
}
}
}
-109
View File
@@ -1,109 +0,0 @@
// 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"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
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-gap='{gap}'
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));
}
}
}
}
-486
View File
@@ -1,486 +0,0 @@
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)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "ComboBox"
name: string = ""
value: string = ""
options: unknown[] = []
groups: unknown[] = []
placeholder: string = "Search or select an option"
searchPlaceholder: string = "Start typing…"
clearable: boolean = true
allowCustomValue: boolean = false
disabled: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
helpText: string = ""
loading: boolean = false
loadingLabel: string = "Loading suggestions…"
emptyLabel: string = "No matching options"
clearLabel: string = "Clear value"
toggleLabel: string = "Toggle suggestions"
searchMode: string = "contains"
searchFields: string = "label,description"
minSearchLength: number = 0
searchResultLimit: number = 0
optionTemplate: string = "default"
defaultOpen: boolean = false
closeOnSelect: boolean = true
fixed: boolean = false
placement: string = "bottom"
autocomplete: string = "off"
remote: boolean = false
remoteUrl: string = ""
remoteQueryParam: string = "q"
remoteDebounce: number = 250
remoteAutoLoad: boolean = true
infinite: boolean = false
hasMore: boolean = false
page: number = 1
loadMoreLabel: string = "Load more"
class: string = ""
}
state open = defaultOpen
state query: string = ""
state selectedValue = value
state activeIndex: number = -1
functions {
shared function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
shared function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
shared function matches(option) {
if (!query || query.length < minSearchLength) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
shared function matchingOptions(list) {
return list.filter((option) => matches(option))
}
shared function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, searchResultLimit)
}
return matchingOptions(list)
}
shared function flatVisibleOptions() {
return visibleOptions(allOptions())
}
shared function isSelected(option) {
return selectedValue === option.value
}
shared function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
shared function selectedText() {
return selectedOptions().length ? selectedOptions()[0].label : selectedValue
}
shared function inputText() {
return query || selectedText()
}
shared function shouldShowDropdown() {
if (!open) {
return false
}
if (loading) {
return true
}
if (remote && query.length !== 0 && query.length < minSearchLength) {
return true
}
if (flatVisibleOptions().length) {
return true
}
return infinite && hasMore
}
shared function canSelect(option) {
return !option.disabled
}
shared function chooseOption(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = option.label
activeIndex = optionIndex(option)
if (closeOnSelect) {
open = false
}
}
client function updateQuery(event) {
query = event.target.value
selectedValue = allowCustomValue ? event.target.value : ""
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
client function clearValue(event) {
if (event) {
event.stopPropagation()
}
query = ""
selectedValue = ""
activeIndex = -1
open = false
}
shared function openDropdown() {
if (!disabled) {
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
}
shared function closeDropdown() {
open = false
}
shared function toggle() {
if (open) {
closeDropdown()
} else {
openDropdown()
}
}
shared function setValue(nextValue) {
selectedValue = nextValue
query = ""
}
shared function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
client function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
openDropdown()
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
openDropdown()
moveActive(-1)
} else if (event.key === "Enter" && open && activeIndex >= 0) {
event.preventDefault()
chooseOption(flatVisibleOptions()[activeIndex])
} else if (event.key === "Escape") {
closeDropdown()
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
shared function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="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'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
data-wrn-combobox
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { closeDropdown() }"
>
<label id="{name}-label" for="{name}-input">{label}</label>
<div class="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="wrn-next__select-trigger wrn-next__combobox-input"
type="text"
role="combobox"
name="{allowCustomValue ? name : ''}"
value="{inputText()}"
placeholder="{placeholder}"
autocomplete="{autocomplete}"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
required="{required && allowCustomValue}"
@focus="openDropdown()"
@click="openDropdown()"
@input="updateQuery(event)"
@keydown="handleKeydown(event)"
/>
<button
type="button"
class="wrn-next__clear-select"
data-show="clearable && inputText() && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearValue(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
<button
type="button"
class="wrn-next__combobox-toggle"
aria-label="{toggleLabel}"
tabindex="-1"
disabled="{disabled}"
@click="toggle()"
>
<span class="wrn-next__select-chevron icon-[lucide--chevrons-up-down]" aria-hidden="true"></span>
</button>
</div>
<div
class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
data-show="shouldShowDropdown()"
style="{shouldShowDropdown() ? '' : 'display: none'}"
>
<div
class="wrn-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<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="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="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="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"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="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="wrn-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="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"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="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="wrn-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
<button
type="button"
class="wrn-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
{#if !allowCustomValue}
<input
type="hidden"
name="{name}"
value="{selectedValue}"
required="{required}"
data-error="{name}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{/if}
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="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;
}
}
}
-72
View File
@@ -1,72 +0,0 @@
component Confetti {
outputs {
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"
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 {
<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 />
</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; }
}
}
}
-526
View File
@@ -1,526 +0,0 @@
component ContextMenu {
outputs {
open(payload: { x: number; y: number; trigger: string; sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
}
props {
items: unknown[] = []
open: boolean = false
defaultOpen: boolean = false
trigger: string = "contextmenu"
placement: string = "pointer"
align: string = "start"
size: string = "default"
color: string = "primary"
variant: string = "raised"
title: string = ""
description: string = ""
label: string = "Context menu"
closeOnSelect: boolean = true
closeOnOutside: boolean = true
disabled: boolean = false
minWidth: string = "14rem"
maxWidth: string = "20rem"
class: string = ""
}
state visible = defaultOpen
state positionX: number = 16
state positionY: number = 16
functions {
shared function isOpen() {
return open || visible
}
client function showMenu(sourceEvent) {
if (disabled) {
return
}
if (sourceEvent && sourceEvent.type === "contextmenu") {
sourceEvent.preventDefault()
}
if (placement === "pointer" && sourceEvent) {
// Place the menu at the pointer and let the anchored clamp in the
// runtime pull it back on screen once it has been laid out and can
// actually be measured. Subtracting a guessed 340x420 here instead
// pushed every menu that was not that size away from the pointer.
positionX = Math.max(12, sourceEvent.clientX || 12)
positionY = Math.max(12, sourceEvent.clientY || 12)
}
visible = true
output.open({
x: positionX,
y: positionY,
trigger: trigger,
sourceEvent: sourceEvent
})
}
client function hideMenu(reason, sourceEvent) {
visible = false
output.close({
reason: reason,
sourceEvent: sourceEvent
})
}
client function toggleMenu(sourceEvent) {
if (isOpen()) {
hideMenu("toggle", sourceEvent)
} else {
showMenu(sourceEvent)
}
}
client function chooseItem(item, itemIndex, sourceEvent) {
if (disabled || item.disabled || item.type === "header" || item.type === "divider") {
sourceEvent.preventDefault()
return
}
output.select({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
if (item.action) {
output.action({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
}
if (closeOnSelect) {
hideMenu("select", sourceEvent)
}
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wrn-context-menu") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
if (!options.length) {
return
}
const activeIndex = options.indexOf(document.activeElement)
const nextIndex = (activeIndex + direction + options.length) % options.length
options[nextIndex].focus()
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hideMenu("escape", sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
moveFocus(sourceEvent, 1)
} else if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
moveFocus(sourceEvent, -1)
}
}
}
view {
<div
{...attrs}
data-ui-component="ContextMenu"
data-open='{open || visible ? "true" : "false"}'
data-trigger='{trigger}'
data-placement='{placement}'
data-align='{align}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='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="wrn-context-menu__trigger"
tabindex='{disabled ? "-1" : "0"}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@contextmenu='if (trigger === "contextmenu" || trigger === "both") { showMenu(event) }'
@click='if (trigger === "click" || trigger === "both") { toggleMenu(event) }'
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggleMenu(event) }'
>
<slot name="trigger"></slot>
</div>
{#if closeOnOutside}
<button
type="button"
class="wrn-context-menu__dismiss-layer"
data-show='{open || visible}'
aria-label="Close context menu"
@click='hideMenu("outside", event)'
></button>
{/if}
<div
class="wrn-context-menu__panel"
data-wrn-anchored="true"
data-show='{open || visible}'
role="menu"
aria-label='{label}'
>
{#if title || description}
<div class="wrn-context-menu__header">
{#if title}
<strong>{title}</strong>
{/if}
{#if description}
<span>{description}</span>
{/if}
</div>
{/if}
<slot name="header"></slot>
<div class="wrn-context-menu__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wrn-context-menu__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wrn-context-menu__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
role="menuitem"
data-context-menu-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
class="wrn-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wrn-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wrn-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wrn-context-menu__status" aria-hidden="true"></span>
{:else if item.external}
<span class="icon-[lucide--arrow-up-right] wrn-context-menu__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
role="menuitem"
data-context-menu-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wrn-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wrn-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wrn-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wrn-context-menu__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
{/each}
</div>
<slot></slot>
<slot name="footer"></slot>
</div>
</div>
}
style {
.wrn-context-menu {
--context-accent: var(--wrn-color-primary);
--context-soft: var(--wrn-color-primary-soft);
position: relative;
display: block;
min-width: 0;
}
.wrn-context-menu[data-color="secondary"] {
--context-accent: var(--wrn-color-secondary);
--context-soft: var(--wrn-color-secondary-soft);
}
.wrn-context-menu[data-color="info"] {
--context-accent: var(--wrn-color-info);
--context-soft: var(--wrn-color-info-soft);
}
.wrn-context-menu[data-color="success"] {
--context-accent: var(--wrn-color-success);
--context-soft: var(--wrn-color-success-soft);
}
.wrn-context-menu[data-color="warning"] {
--context-accent: var(--wrn-color-warning);
--context-soft: var(--wrn-color-warning-soft);
}
.wrn-context-menu[data-color="danger"] {
--context-accent: var(--wrn-color-danger);
--context-soft: var(--wrn-color-danger-soft);
}
.wrn-context-menu__trigger {
display: block;
min-width: 0;
outline: none;
}
.wrn-context-menu__trigger:focus-visible {
border-radius: 0.6rem;
outline: 2px solid var(--wrn-color-focus);
outline-offset: 3px;
}
.wrn-context-menu__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1090;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wrn-context-menu__panel {
position: absolute;
z-index: 1091;
top: calc(100% + 0.5rem);
left: 0;
width: max-content;
min-width: var(--wrn-context-min-width);
max-width: min(var(--wrn-context-max-width), calc(100vw - 1.5rem));
padding: 0.45rem;
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(--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,
0 24px 64px color-mix(in srgb, black 24%, transparent);
backdrop-filter: blur(18px);
transform-origin: top left;
}
.wrn-context-menu[data-placement="pointer"] .wrn-context-menu__panel {
position: fixed;
top: var(--wrn-context-y);
left: var(--wrn-context-x);
}
.wrn-context-menu[data-placement="bottom-end"] .wrn-context-menu__panel {
right: 0;
left: auto;
transform-origin: top right;
}
.wrn-context-menu[data-placement="top-start"] .wrn-context-menu__panel {
top: auto;
bottom: calc(100% + 0.5rem);
transform-origin: bottom left;
}
.wrn-context-menu[data-placement="top-end"] .wrn-context-menu__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.5rem);
left: auto;
transform-origin: bottom right;
}
.wrn-context-menu[data-variant="soft"] .wrn-context-menu__panel {
background:
linear-gradient(145deg, var(--context-soft), transparent 72%),
var(--wrn-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wrn-context-menu[data-variant="outline"] .wrn-context-menu__panel {
background: var(--wrn-color-surface-raised);
box-shadow: none;
}
.wrn-context-menu__header {
display: grid;
gap: 0.2rem;
padding: 0.65rem 0.75rem 0.75rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-context-menu__header strong {
font-size: 0.86rem;
font-weight: 650;
}
.wrn-context-menu__header span {
color: var(--wrn-color-text-muted);
font-size: 0.75rem;
line-height: 1.45;
}
.wrn-context-menu__items {
display: grid;
gap: 0.15rem;
padding-block: 0.25rem;
}
.wrn-context-menu__section-label {
padding: 0.55rem 0.75rem 0.3rem;
color: var(--wrn-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wrn-context-menu__divider {
height: 1px;
margin: 0.3rem 0.45rem;
background: var(--wrn-color-border);
}
.wrn-context-menu__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.7rem;
width: 100%;
min-width: 0;
padding: 0.65rem 0.7rem;
color: var(--wrn-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
}
.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;
}
.wrn-context-menu__item:active {
transform: scale(0.985);
}
.wrn-context-menu__item[data-danger="true"] {
color: var(--wrn-color-danger);
}
.wrn-context-menu__item[data-danger="true"]:hover,
.wrn-context-menu__item[data-danger="true"]:focus-visible {
background: var(--wrn-color-danger-soft);
}
.wrn-context-menu__item[disabled],
.wrn-context-menu__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wrn-context-menu__icon,
.wrn-context-menu__status {
width: 1rem;
height: 1rem;
color: currentColor;
}
.wrn-context-menu__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wrn-context-menu__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrn-context-menu__copy small {
overflow: hidden;
color: var(--wrn-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrn-context-menu kbd {
padding: 0.16rem 0.38rem;
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;
}
.wrn-context-menu[data-size="sm"] .wrn-context-menu__item {
padding: 0.52rem 0.6rem;
}
.wrn-context-menu[data-size="lg"] .wrn-context-menu__item {
padding: 0.78rem 0.82rem;
}
@media (max-width: 639px) {
.wrn-context-menu[data-placement="pointer"] .wrn-context-menu__panel {
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
max-width: none;
}
}
@media (prefers-reduced-motion: reduce) {
.wrn-context-menu__item {
transition: none;
}
}
}
}
-122
View File
@@ -1,122 +0,0 @@
// 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 {
props {
color: string = "primary"
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
{...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;
}
}
}
}
-108
View File
@@ -1,108 +0,0 @@
component DataMap {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Data Map"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="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="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
-678
View File
@@ -1,678 +0,0 @@
component DatePicker {
outputs {
input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
open(payload: { value: string; name: string; sourceEvent: Event })
close(payload: { value: string; name: string; sourceEvent: Event })
focus(payload: { value: string; name: string; sourceEvent: Event })
blur(payload: { value: string; name: string; sourceEvent: Event })
invalid(payload: { name: string; message: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Date Picker"
id: string = ""
name: string = ""
value: string = ""
placeholder: string = ""
type: string = "date"
locale: string = "en-US"
firstDayOfWeek: number = 0
months = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
years = [2024, 2025, 2026, 2027, 2028, 2029, 2030]
min: string = ""
max: string = ""
step: string = ""
helperText: string = ""
cornerHint: string = ""
error: string = ""
variant: string = "normal"
inline: boolean = false
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state currentValue = value
state selectedYear = value ? value.split("-")[0] : ""
state selectedMonth = value ? value.split("-")[1] : ""
state selectedDay = value ? value.split("-")[2] : ""
state expanded: boolean = false
functions {
client function openCalendar(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function updateDate(part, nextValue, sourceEvent, nextDate, detail) { if (part === "year") { selectedYear = String(nextValue) } if (part === "month") { selectedMonth = String(nextValue).padStart(2, "0") } if (part === "day") { selectedDay = String(nextValue).padStart(2, "0") } if (!selectedYear || !selectedMonth || !selectedDay) { return } nextDate = selectedYear + "-" + selectedMonth + "-" + selectedDay; if ((min && nextDate < min) || (max && nextDate > max)) { return } currentValue = nextDate; detail = { value: currentValue, year: selectedYear, month: selectedMonth, day: selectedDay, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function finishDate(sourceEvent) { if (!currentValue) { return } expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".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="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);
}
}
}
-176
View File
@@ -1,176 +0,0 @@
component DeviceFrame {
outputs {
change(payload: { device: string; orientation: string; sourceEvent: Event })
rotate(payload: { device: string; orientation: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Device Frame"
description: string = ""
items: unknown[] = []
variant: string = "default"
device: string = "phone"
orientation: string = "portrait"
src: string = ""
srcdoc: string = ""
frameTitle: string = "Device preview"
showToolbar: boolean = true
allow: string = ""
class: string = ""
}
state currentOrientation = orientation
functions {
client function setDevice(nextDevice, sourceEvent) { output.change({ device: nextDevice, orientation: currentOrientation, sourceEvent: sourceEvent }) }
client function rotateFrame(sourceEvent) { currentOrientation = currentOrientation === "portrait" ? "landscape" : "portrait"; output.rotate({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }); output.change({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }) }
}
view {
<section {...attrs} class="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;
}
}
}
-112
View File
@@ -1,112 +0,0 @@
component DragAndDrop {
outputs {
dragStart(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragEnd(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragEnter(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragLeave(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
drop(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Drag And Drop"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="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="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;
}
}
}
-535
View File
@@ -1,535 +0,0 @@
component Drawer {
outputs {
open(payload: { placement: string; sourceEvent: Event })
close(payload: { reason: string; placement: string; sourceEvent: Event })
cancel(payload: { placement: string; sourceEvent: Event })
}
props {
open: boolean = false
defaultOpen: boolean = false
placement: string = "right"
size: string = "md"
color: string = "primary"
variant: string = "default"
title: string = "Drawer"
description: string = ""
icon: string = ""
label: string = "Drawer"
closeLabel: string = "Close drawer"
showClose: boolean = true
closeOnBackdrop: boolean = true
closeOnEscape: boolean = true
// Open/close animation length in ms. 0 disables the animation entirely.
duration: number = 260
overlay: boolean = true
scrollable: boolean = true
triggerLabel: string = ""
triggerIcon: string = ""
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showDrawer(sourceEvent) {
visible = true
output.open({
placement: placement,
sourceEvent: sourceEvent
})
}
client function hideDrawer(reason, sourceEvent) {
visible = false
output.close({
reason: reason,
placement: placement,
sourceEvent: sourceEvent
})
}
client function cancelDrawer(sourceEvent) {
output.cancel({
placement: placement,
sourceEvent: sourceEvent
})
hideDrawer("cancel", sourceEvent)
}
client function handleKeydown(sourceEvent) {
if (closeOnEscape && sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
cancelDrawer(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-ui-component="Drawer"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-overlay='{overlay ? "true" : "false"}'
data-scrollable='{scrollable ? "true" : "false"}'
class='wrn-drawer {class}'
style='--drawer-duration: {duration}ms'
>
{#if triggerLabel}
<button
type="button"
class="wrn-drawer__trigger"
aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}'
@click='showDrawer(event)'
>
{#if triggerIcon}
<span class='{triggerIcon}' aria-hidden="true"></span>
{/if}
<span>{triggerLabel}</span>
</button>
{/if}
<span class="wrn-drawer__trigger-slot" @click='showDrawer(event)'>
<slot name="trigger"></slot>
</span>
<div
class="wrn-drawer__layer"
role="presentation"
@keydown='handleKeydown(event)'
>
{#if overlay}
<button
type="button"
class="wrn-drawer__backdrop"
aria-label='{closeLabel}'
@click='if (closeOnBackdrop) { cancelDrawer(event) }'
></button>
{/if}
<section
class="wrn-drawer__panel"
role="dialog"
aria-modal="true"
aria-label='{label || title}'
tabindex="-1"
>
<div class="wrn-drawer__handle" aria-hidden="true"></div>
<header class="wrn-drawer__header">
<div class="wrn-drawer__heading">
{#if icon}
<span class='wrn-drawer__icon {icon}' aria-hidden="true"></span>
{/if}
<div class="wrn-drawer__heading-copy">
<slot name="header"></slot>
{#if title}
<h2>{title}</h2>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
</div>
{#if showClose}
<button
type="button"
class="wrn-drawer__close"
aria-label='{closeLabel}'
@click='hideDrawer("close-button", event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</header>
<div class="wrn-drawer__body">
<slot></slot>
</div>
<footer class="wrn-drawer__footer">
<slot name="footer"></slot>
</footer>
</section>
</div>
</div>
}
style {
.wrn-drawer {
--drawer-accent: var(--wrn-color-primary);
--drawer-soft: var(--wrn-color-primary-soft);
position: relative;
display: inline-flex;
}
.wrn-drawer[data-color="secondary"] {
--drawer-accent: var(--wrn-color-secondary);
--drawer-soft: var(--wrn-color-secondary-soft);
}
.wrn-drawer[data-color="info"] {
--drawer-accent: var(--wrn-color-info);
--drawer-soft: var(--wrn-color-info-soft);
}
.wrn-drawer[data-color="success"] {
--drawer-accent: var(--wrn-color-success);
--drawer-soft: var(--wrn-color-success-soft);
}
.wrn-drawer[data-color="warning"] {
--drawer-accent: var(--wrn-color-warning);
--drawer-soft: var(--wrn-color-warning-soft);
}
.wrn-drawer[data-color="danger"] {
--drawer-accent: var(--wrn-color-danger);
--drawer-soft: var(--wrn-color-danger-soft);
}
.wrn-drawer__trigger,
.wrn-drawer__trigger-slot {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.wrn-drawer__trigger {
appearance: none;
min-height: 2.55rem;
padding: 0.65rem 1rem;
color: var(--wrn-color-primary-contrast);
background: var(--drawer-accent);
border: 0;
border-radius: 0.8rem;
font: inherit;
font-size: 0.85rem;
font-weight: 650;
cursor: pointer;
}
/*
* The layer stays in the layout and is revealed by [data-open]; it used to
* be toggled with data-show, which sets display:none, and display cannot
* be transitioned -- the drawer simply snapped in and out. visibility is
* 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.
*/
.wrn-drawer__layer {
position: fixed;
inset: 0;
z-index: 1200;
display: flex;
pointer-events: none;
visibility: hidden;
opacity: 0;
transition:
opacity var(--drawer-duration, 260ms) ease,
visibility 0s linear var(--drawer-duration, 260ms);
}
.wrn-drawer[data-open="true"] .wrn-drawer__layer {
visibility: visible;
opacity: 1;
transition:
opacity var(--drawer-duration, 260ms) ease,
visibility 0s linear 0s;
}
.wrn-drawer__backdrop {
position: absolute;
inset: 0;
z-index: 0;
appearance: none;
padding: 0;
background: color-mix(in srgb, black 56%, transparent);
border: 0;
backdrop-filter: blur(7px);
pointer-events: auto;
}
.wrn-drawer[data-overlay="false"] .wrn-drawer__backdrop {
display: none;
}
.wrn-drawer__panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(28rem, calc(100vw - 1rem));
max-height: 100%;
margin-left: auto;
color: var(--wrn-color-text);
background:
radial-gradient(
circle at 100% 0%,
color-mix(in srgb, var(--drawer-accent) 8%, transparent),
transparent 34%
),
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;
/* Slides in from whichever edge the placement puts it on. */
transform: translateX(100%);
transition: transform var(--drawer-duration, 260ms) cubic-bezier(0.32, 0.72, 0, 1);
}
.wrn-drawer[data-open="true"] .wrn-drawer__panel {
transform: none;
}
.wrn-drawer[data-placement="left"] .wrn-drawer__panel {
transform: translateX(-100%);
}
.wrn-drawer[data-placement="top"] .wrn-drawer__panel {
transform: translateY(-100%);
}
.wrn-drawer[data-placement="bottom"] .wrn-drawer__panel {
transform: translateY(100%);
}
.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) {
.wrn-drawer__layer,
.wrn-drawer__panel {
transition: none;
}
}
.wrn-drawer[data-size="sm"] .wrn-drawer__panel {
width: min(22rem, calc(100vw - 1rem));
}
.wrn-drawer[data-size="lg"] .wrn-drawer__panel {
width: min(38rem, calc(100vw - 1rem));
}
.wrn-drawer[data-size="xl"] .wrn-drawer__panel {
width: min(52rem, calc(100vw - 1rem));
}
.wrn-drawer[data-size="full"] .wrn-drawer__panel {
width: 100vw;
}
.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(--wrn-color-border));
border-left: 0;
box-shadow: 28px 0 80px color-mix(in srgb, black 28%, transparent);
}
.wrn-drawer[data-placement="top"] .wrn-drawer__layer,
.wrn-drawer[data-placement="bottom"] .wrn-drawer__layer {
align-items: flex-start;
}
.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;
border: 0;
box-shadow: 0 24px 80px color-mix(in srgb, black 28%, transparent);
}
.wrn-drawer[data-placement="top"] .wrn-drawer__panel {
border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-color-border));
}
.wrn-drawer[data-placement="bottom"] .wrn-drawer__layer {
align-items: flex-end;
}
.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;
}
.wrn-drawer[data-variant="soft"] .wrn-drawer__panel {
background:
linear-gradient(145deg, var(--drawer-soft), transparent 65%),
var(--wrn-color-surface-raised);
}
.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);
}
.wrn-drawer__handle {
display: none;
width: 2.75rem;
height: 0.28rem;
margin: 0.55rem auto 0;
background: color-mix(in srgb, var(--wrn-color-text-muted) 48%, transparent);
border-radius: 999px;
}
.wrn-drawer[data-placement="bottom"] .wrn-drawer__handle {
display: block;
}
.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(--wrn-color-border);
}
.wrn-drawer__heading {
display: flex;
align-items: flex-start;
gap: 0.85rem;
min-width: 0;
}
.wrn-drawer__icon {
flex: 0 0 auto;
width: 1.25rem;
height: 1.25rem;
margin-top: 0.15rem;
color: var(--drawer-accent);
}
.wrn-drawer[data-variant="solid"] .wrn-drawer__icon {
color: currentColor;
}
.wrn-drawer__heading-copy {
display: grid;
gap: 0.3rem;
min-width: 0;
}
.wrn-drawer__heading-copy h2,
.wrn-drawer__heading-copy p {
margin: 0;
}
.wrn-drawer__heading-copy h2 {
font-size: 1.05rem;
font-weight: 650;
line-height: 1.3;
}
.wrn-drawer__heading-copy p {
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
line-height: 1.55;
}
.wrn-drawer[data-variant="solid"] .wrn-drawer__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
/*
* padding is reset explicitly: an app-level `button { padding: ... }` rule
* outranks the browser default and leaves this fixed-size button with a
* 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.
*/
.wrn-drawer__close {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0;
width: 2.35rem;
height: 2.35rem;
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;
}
.wrn-drawer__close:hover,
.wrn-drawer__close:focus-visible {
color: var(--drawer-accent);
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. */
.wrn-drawer__close svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
}
/*
* Slot content is authored by the host app, so the app global stylesheet
* styles it too. A bare element selector there (p { color: ... }) beats
* anything the panel merely *inherits*, which is how modal body copy ended
* up muted grey on a saturated background. State the colour explicitly;
* :where() keeps the specificity low enough that any class the app puts on
* its own slot content still wins.
*/
.wrn-drawer__body {
flex: 1 1 auto;
min-height: 0;
padding: 1.35rem;
color: var(--wrn-color-text);
}
.wrn-drawer__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
color: inherit;
}
.wrn-drawer[data-scrollable="true"] .wrn-drawer__body {
overflow: auto;
overscroll-behavior: contain;
}
.wrn-drawer__footer {
padding: 1rem 1.35rem 1.35rem;
border-top: 1px solid var(--wrn-color-border);
}
.wrn-drawer__footer:empty {
display: none;
}
@media (max-width: 639px) {
.wrn-drawer[data-placement="right"] .wrn-drawer__panel,
.wrn-drawer[data-placement="left"] .wrn-drawer__panel {
width: min(24rem, 100vw);
}
.wrn-drawer__header,
.wrn-drawer__body,
.wrn-drawer__footer {
padding-inline: 1rem;
}
}
}
}
-573
View File
@@ -1,573 +0,0 @@
component Dropdown {
outputs {
toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
open(payload: { sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
}
props {
items: unknown[] = []
open: boolean = false
defaultOpen: boolean = false
label: string = "Open menu"
icon: string = ""
showChevron: boolean = true
menuLabel: string = "Dropdown menu"
placement: string = "bottom-start"
width: string = "md"
size: string = "default"
color: string = "primary"
variant: string = "raised"
closeOnSelect: boolean = true
closeOnOutside: boolean = true
disabled: boolean = false
emptyLabel: string = "No menu items"
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showMenu(sourceEvent) {
if (disabled) {
return
}
visible = true
output.open({ sourceEvent: sourceEvent })
output.toggle({ open: true, sourceEvent: sourceEvent })
}
client function hideMenu(reason, sourceEvent) {
visible = false
output.close({ reason: reason, sourceEvent: sourceEvent })
output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
}
client function toggleMenu(sourceEvent) {
if (isOpen()) {
hideMenu("toggle", sourceEvent)
} else {
showMenu(sourceEvent)
}
}
client function chooseItem(item, itemIndex, sourceEvent) {
if (disabled || item.disabled || item.type === "divider" || item.type === "header") {
sourceEvent.preventDefault()
return
}
output.select({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
if (item.action) {
output.action({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
}
if (closeOnSelect) {
hideMenu("select", sourceEvent)
}
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (!options.length) {
return
}
const activeIndex = options.indexOf(document.activeElement)
const nextIndex = (activeIndex + direction + options.length) % options.length
options[nextIndex].focus()
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hideMenu("escape", sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
if (!isOpen()) {
showMenu(sourceEvent)
}
moveFocus(sourceEvent, 1)
} else if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
if (!isOpen()) {
showMenu(sourceEvent)
}
moveFocus(sourceEvent, -1)
} else if (sourceEvent.key === "Home" && isOpen()) {
sourceEvent.preventDefault()
const root = sourceEvent.currentTarget.closest(".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(".wrn-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (options.length) {
options[options.length - 1].focus()
}
}
}
}
view {
<div
{...attrs}
data-ui-component="Dropdown"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-width='{width}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wrn-dropdown {class}'
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wrn-dropdown__trigger"
disabled='{disabled}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@click='toggleMenu(event)'
>
<slot name="trigger"></slot>
{#if icon}
<span class='wrn-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
{/if}
{#if label}
<span class="wrn-dropdown__trigger-label">{label}</span>
{/if}
{#if showChevron}
<span class="icon-[lucide--chevron-down] wrn-dropdown__chevron" aria-hidden="true"></span>
{/if}
</button>
{#if closeOnOutside}
<button
type="button"
class="wrn-dropdown__dismiss-layer"
data-show='{open || visible}'
aria-label="Close menu"
@click='hideMenu("outside", event)'
></button>
{/if}
<div
class="wrn-dropdown__panel"
data-wrn-anchored="true"
data-show='{open || visible}'
role="menu"
aria-label='{menuLabel}'
>
<slot name="header"></slot>
<div class="wrn-dropdown__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wrn-dropdown__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wrn-dropdown__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
role="menuitem"
data-dropdown-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
class="wrn-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wrn-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<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="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] wrn-dropdown__status" aria-hidden="true"></span>
{:else if item.external}
<span class="icon-[lucide--arrow-up-right] wrn-dropdown__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
role="menuitem"
data-dropdown-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wrn-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wrn-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<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="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] wrn-dropdown__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
{:empty}
<div class="wrn-dropdown__empty">{emptyLabel}</div>
{/each}
</div>
<slot></slot>
<slot name="footer"></slot>
</div>
</div>
}
style {
.wrn-dropdown {
--dropdown-accent: var(--wrn-color-primary);
--dropdown-soft: var(--wrn-color-primary-soft);
position: relative;
display: inline-flex;
min-width: 0;
}
.wrn-dropdown[data-color="secondary"] {
--dropdown-accent: var(--wrn-color-secondary);
--dropdown-soft: var(--wrn-color-secondary-soft);
}
.wrn-dropdown[data-color="info"] {
--dropdown-accent: var(--wrn-color-info);
--dropdown-soft: var(--wrn-color-info-soft);
}
.wrn-dropdown[data-color="success"] {
--dropdown-accent: var(--wrn-color-success);
--dropdown-soft: var(--wrn-color-success-soft);
}
.wrn-dropdown[data-color="warning"] {
--dropdown-accent: var(--wrn-color-warning);
--dropdown-soft: var(--wrn-color-warning-soft);
}
.wrn-dropdown[data-color="danger"] {
--dropdown-accent: var(--wrn-color-danger);
--dropdown-soft: var(--wrn-color-danger-soft);
}
.wrn-dropdown__trigger {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.55rem;
min-height: 2.55rem;
padding: 0.65rem 0.9rem;
color: var(--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;
font-weight: 600;
cursor: pointer;
transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease;
}
.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(--wrn-color-border));
outline: none;
}
.wrn-dropdown__trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.wrn-dropdown__trigger-icon,
.wrn-dropdown__chevron {
width: 1rem;
height: 1rem;
}
.wrn-dropdown__chevron {
transition: transform 160ms ease;
}
.wrn-dropdown[data-open="true"] .wrn-dropdown__chevron {
transform: rotate(180deg);
}
.wrn-dropdown[data-size="sm"] .wrn-dropdown__trigger {
min-height: 2.2rem;
padding: 0.5rem 0.72rem;
font-size: 0.78rem;
}
.wrn-dropdown[data-size="lg"] .wrn-dropdown__trigger {
min-height: 2.9rem;
padding: 0.78rem 1.05rem;
font-size: 0.9rem;
}
.wrn-dropdown__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1100;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wrn-dropdown__panel {
position: absolute;
z-index: 1101;
top: calc(100% + 0.55rem);
left: 0;
width: max-content;
min-width: 12rem;
max-width: min(22rem, calc(100vw - 1.5rem));
padding: 0.45rem;
color: var(--wrn-color-text);
background:
linear-gradient(145deg, color-mix(in srgb, var(--dropdown-accent) 5%, transparent), transparent 60%),
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,
0 24px 64px color-mix(in srgb, black 22%, transparent);
backdrop-filter: blur(18px);
}
.wrn-dropdown[data-width="trigger"] .wrn-dropdown__panel {
width: 100%;
min-width: 100%;
}
.wrn-dropdown[data-width="sm"] .wrn-dropdown__panel {
width: 12rem;
}
.wrn-dropdown[data-width="md"] .wrn-dropdown__panel {
width: 17rem;
}
.wrn-dropdown[data-width="lg"] .wrn-dropdown__panel {
width: 22rem;
}
.wrn-dropdown[data-placement="bottom-end"] .wrn-dropdown__panel {
right: 0;
left: auto;
}
.wrn-dropdown[data-placement="top-start"] .wrn-dropdown__panel {
top: auto;
bottom: calc(100% + 0.55rem);
}
.wrn-dropdown[data-placement="top-end"] .wrn-dropdown__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.55rem);
left: auto;
}
.wrn-dropdown[data-variant="soft"] .wrn-dropdown__panel {
background:
linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
var(--wrn-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wrn-dropdown[data-variant="outline"] .wrn-dropdown__panel {
background: var(--wrn-color-surface-raised);
box-shadow: none;
}
.wrn-dropdown__items {
display: grid;
gap: 0.15rem;
}
.wrn-dropdown__section-label {
padding: 0.55rem 0.72rem 0.3rem;
color: var(--wrn-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wrn-dropdown__divider {
height: 1px;
margin: 0.3rem 0.4rem;
background: var(--wrn-color-border);
}
.wrn-dropdown__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.7rem;
width: 100%;
min-width: 0;
padding: 0.68rem 0.72rem;
color: var(--wrn-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
}
.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;
}
.wrn-dropdown__item:active {
transform: scale(0.985);
}
.wrn-dropdown__item[data-danger="true"] {
color: var(--wrn-color-danger);
}
.wrn-dropdown__item[data-danger="true"]:hover,
.wrn-dropdown__item[data-danger="true"]:focus-visible {
background: var(--wrn-color-danger-soft);
}
.wrn-dropdown__item[disabled],
.wrn-dropdown__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wrn-dropdown__item-icon,
.wrn-dropdown__status {
width: 1rem;
height: 1rem;
}
.wrn-dropdown__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wrn-dropdown__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrn-dropdown__copy small {
overflow: hidden;
color: var(--wrn-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrn-dropdown__badge {
padding: 0.18rem 0.45rem;
color: var(--dropdown-accent);
background: var(--dropdown-soft);
border-radius: 999px;
font-size: 0.64rem;
font-weight: 700;
}
.wrn-dropdown kbd {
padding: 0.16rem 0.38rem;
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;
}
.wrn-dropdown__empty {
padding: 1rem;
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
text-align: center;
}
@media (max-width: 639px) {
.wrn-dropdown__panel {
position: fixed;
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
min-width: 0;
max-width: none;
}
}
@media (prefers-reduced-motion: reduce) {
.wrn-dropdown__trigger,
.wrn-dropdown__chevron,
.wrn-dropdown__item {
transition: none;
}
}
}
}
-632
View File
@@ -1,632 +0,0 @@
component FileInput {
outputs {
input(payload: { files: File[]; name: string; sourceEvent: Event })
change(payload: { files: File[]; name: string; sourceEvent: Event })
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { files: File[]; name: string; sourceEvent: Event })
clear(payload: { name: string; sourceEvent: Event })
invalid(payload: { message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "File"
hiddenLabel: boolean = false
placeholder: string = "Choose a file"
value: string = ""
icon: string = "icon-[lucide--upload]"
iconPosition: string = "start"
accept: string = ""
multiple: boolean = false
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state selectedName: string = ""
functions {
client function handleChange(sourceEvent) {
sourceEvent.stopPropagation()
selectedName = sourceEvent.currentTarget.files && sourceEvent.currentTarget.files.length ? sourceEvent.currentTarget.files[0].name : ""
output.input({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
output.change({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
if (selectedName) output.select({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
else output.clear({ name: name, sourceEvent: sourceEvent })
}
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ name: name, sourceEvent: sourceEvent }) }
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="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,646 +0,0 @@
component FileUploadProgress {
outputs {
cancel(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
retry(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
complete(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
label: string = "Progress"
value: number = 50
max: number = 100
showValue: boolean = true
fileName: string = ""
fileSize: string = ""
uploadedSize: string = ""
status: string = "uploading"
cancelLabel: string = "Cancel upload"
retryLabel: string = "Retry upload"
class: string = ""
}
state currentValue = value
state currentStatus = status
functions {
shared function percent() { if (!Number(max)) { return 0 } return Math.max(0, Math.min(100, Math.round(Number(currentValue) / Number(max) * 100))) }
client function detail(sourceEvent) { return { value: currentValue, max: max, percent: percent(), status: currentStatus, fileName: fileName, sourceEvent: sourceEvent } }
client function cancelUpload(sourceEvent) { currentStatus = "cancelled"; output.cancel(detail(sourceEvent)) }
client function retryUpload(sourceEvent) { currentValue = 0; currentStatus = "uploading"; output.retry(detail(sourceEvent)) }
client function completeUpload(sourceEvent) { currentValue = max; currentStatus = "complete"; output.complete(detail(sourceEvent)) }
}
view {
<div {...attrs} class="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="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);
}
}
}
-127
View File
@@ -1,127 +0,0 @@
// 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"
color: string = "primary"
src: string = ""
alt: string = ""
width: string = ""
height: string = ""
loading: string = "lazy"
fit: string = "cover"
rounded: boolean = false
class: string = ""
}
view {
<figure
{...attrs}
data-ui-component="Image"
class='wrn-image {class}'
data-size='{size}'
data-color='{color}'
data-fit='{fit}'
data-rounded='{rounded}'
>
<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;
}
}
}
-645
View File
@@ -1,645 +0,0 @@
component InputGroup {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
submit(payload: { value: string | number | boolean | null | object; name: string; sourceEvent: Event })
action(payload: { name: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Input group"
hiddenLabel: boolean = false
value: string = ""
placeholder: string = ""
type: string = "text"
startText: string = ""
endText: string = ""
icon: string = ""
iconPosition: string = "start"
actionLabel: string = ""
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
functions {
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
client function handleKeydown(sourceEvent) { if (sourceEvent.key === "Enter") { sourceEvent.stopPropagation(); output.submit({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) } }
client function handleAction(sourceEvent) { output.action({ name: name, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="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="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="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);
}
}
}
-682
View File
@@ -1,682 +0,0 @@
component InputNumber {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
increment(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
decrement(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
variant: string = "default"
class: string = ""
id: string = ""
name: string = "quantity"
value: number = 0
min: string = ""
max: string = ""
step: number = 1
precision: string = "auto"
label: string = ""
description: string = ""
helpText: string = ""
error: string = ""
invalid: boolean = false
prefix: string = ""
suffix: string = ""
placeholder: string = ""
autocomplete: string = "off"
inputMode: string = "decimal"
ariaLabel: string = ""
required: boolean = false
disabled: boolean = false
inputDisabled: boolean = false
buttonsDisabled: boolean = false
readonly: boolean = false
allowInput: boolean = true
keyboard: boolean = true
wheel: boolean = false
clamp: boolean = true
fullWidth: boolean = false
showButtons: boolean = true
showValidationMessage: boolean = true
decrementLabel: string = "Decrease value"
incrementLabel: string = "Increase value"
controlsLabel: string = "Quantity controls"
requiredMessage: string = "A value is required."
minMessage: string = "Value is below the minimum."
maxMessage: string = "Value is above the maximum."
}
state currentValue = value
state committedValue = value
functions {
shared function inputId() {
if (id !== "") {
return id
}
if (name !== "") {
return name
}
return "input-number"
}
shared function descriptionId() {
return inputId() + "-description"
}
shared function messageId() {
return inputId() + "-message"
}
shared function componentColor() {
if (color === "secondary") {
return "var(--wrn-color-secondary)"
}
if (color === "success") {
return "var(--wrn-color-success)"
}
if (color === "warning") {
return "var(--wrn-color-warning)"
}
if (color === "danger") {
return "var(--wrn-color-danger)"
}
if (color === "info") {
return "var(--wrn-color-info)"
}
return "var(--wrn-color-primary)"
}
shared function hasMin() {
return min !== "" && min !== null && min !== undefined
}
shared function hasMax() {
return max !== "" && max !== null && max !== undefined
}
shared function isBlank() {
return currentValue === "" || currentValue === null || currentValue === undefined
}
shared function normalizedStep() {
return Number(step) > 0 ? Number(step) : 1
}
shared function inferredPrecision() {
if (precision !== "auto" && precision !== "") {
return Math.max(0, Number(precision) || 0)
}
if (String(normalizedStep()).includes(".")) {
return String(normalizedStep()).split(".")[1].length
}
return 0
}
shared function precisionFactor() {
return Math.pow(10, inferredPrecision())
}
shared function roundValue(nextValue) {
return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor()
}
shared function clampValue(nextValue) {
nextValue = Number(nextValue)
if (hasMin() && nextValue < Number(min)) {
nextValue = Number(min)
}
if (hasMax() && nextValue > Number(max)) {
nextValue = Number(max)
}
return roundValue(nextValue)
}
shared function isBelowMin() {
return !isBlank() && hasMin() && Number(currentValue) < Number(min)
}
shared function isAboveMax() {
return !isBlank() && hasMax() && Number(currentValue) > Number(max)
}
shared function isInvalid() {
return (
invalid ||
error !== "" ||
(required && isBlank()) ||
isBelowMin() ||
isAboveMax()
)
}
shared function validationMessage() {
if (error !== "") {
return error
}
if (required && isBlank()) {
return requiredMessage
}
if (isBelowMin()) {
return minMessage
}
if (isAboveMax()) {
return maxMessage
}
return ""
}
shared function hasMessage() {
return (
helpText !== "" ||
(showValidationMessage && isInvalid() && validationMessage() !== "")
)
}
shared function describedBy() {
if (description !== "" && hasMessage()) {
return descriptionId() + " " + messageId()
}
if (description !== "") {
return descriptionId()
}
if (hasMessage()) {
return messageId()
}
return ""
}
shared function decrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMin() && !isBlank() && Number(currentValue) <= Number(min))
)
}
shared function incrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMax() && !isBlank() && Number(currentValue) >= Number(max))
)
}
// 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,
payload
) {
payload = {
component: "InputNumber",
name: name,
value: currentValue,
previousValue: previousValue,
action: action,
min: hasMin() ? Number(min) : null,
max: hasMax() ? Number(max) : null,
step: normalizedStep(),
valid: !isInvalid()
}
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) {
previousValue = currentValue
currentValue = clampValue(nextValue)
committedValue = currentValue
dispatchInputNumberEvent(
sourceEvent,
"input",
action,
previousValue
)
dispatchInputNumberEvent(
sourceEvent,
"change",
action,
previousValue
)
dispatchInputNumberEvent(
sourceEvent,
action,
action,
previousValue
)
}
client function incrementValue(sourceEvent, nextValue) {
if (incrementDisabled()) {
return
}
if (isBlank()) {
nextValue = hasMin() ? Number(min) : normalizedStep()
} else {
nextValue =
Number(currentValue) +
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
}
applyControlValue(nextValue, "increment", sourceEvent)
}
client function decrementValue(sourceEvent, nextValue) {
if (decrementDisabled()) {
return
}
if (isBlank()) {
nextValue = hasMax() ? Number(max) : -normalizedStep()
} else {
nextValue =
Number(currentValue) -
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
}
applyControlValue(nextValue, "decrement", sourceEvent)
}
client function handleInput(sourceEvent, previousValue, nextValue) {
sourceEvent.stopPropagation()
previousValue = currentValue
nextValue = sourceEvent.target.value
if (nextValue === "") {
currentValue = ""
} else if (!Number.isNaN(Number(nextValue))) {
currentValue = roundValue(Number(nextValue))
}
dispatchInputNumberEvent(
sourceEvent,
"input",
"input",
previousValue
)
}
client function handleChange(sourceEvent, previousValue) {
sourceEvent.stopPropagation()
previousValue = committedValue
if (!isBlank()) {
currentValue = clamp
? clampValue(currentValue)
: roundValue(currentValue)
}
committedValue = currentValue
dispatchInputNumberEvent(
sourceEvent,
"change",
"change",
previousValue
)
}
client function handleKeydown(sourceEvent) {
if (
!keyboard ||
disabled ||
readonly ||
inputDisabled ||
!allowInput
) {
return
}
if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
incrementValue(sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
decrementValue(sourceEvent)
} else if (sourceEvent.key === "Home" && hasMin()) {
sourceEvent.preventDefault()
applyControlValue(Number(min), "decrement", sourceEvent)
} else if (sourceEvent.key === "End" && hasMax()) {
sourceEvent.preventDefault()
applyControlValue(Number(max), "increment", sourceEvent)
}
}
client function handleWheel(sourceEvent) {
if (
!wheel ||
disabled ||
readonly ||
inputDisabled ||
!allowInput
) {
return
}
sourceEvent.preventDefault()
if (sourceEvent.deltaY < 0) {
incrementValue(sourceEvent)
} else if (sourceEvent.deltaY > 0) {
decrementValue(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-wrn-input-number
data-variant='{variant}'
data-size='{size}'
data-color='{color}'
data-value='{currentValue}'
data-invalid='{isInvalid() ? "true" : "false"}'
data-disabled='{disabled ? "true" : "false"}'
data-full-width='{fullWidth ? "true" : "false"}'
style='--input-number-accent: {componentColor()};'
class='wrn-input-number {class}'
>
{#if label !== "" && variant !== "labeled" && variant !== "seat"}
<label
for='{inputId()}'
class='wrn-input-number__label'
>
<span>{label}</span>
{#if required}
<span
aria-hidden="true"
class='wrn-input-number__required'
>*</span>
{/if}
</label>
{/if}
{#if description !== "" && variant !== "labeled" && variant !== "seat"}
<p
id='{descriptionId()}'
class='wrn-input-number__description'
>
{description}
</p>
{/if}
<div
class='wrn-input-number__control'
>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='wrn-input-number__button wrn-input-number__button--start'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] wrn-input-number__icon'
></span>
</button>
{/if}
<div
class='wrn-input-number__content'
>
{#if variant === "labeled" || variant === "seat"}
<div class='wrn-input-number__copy'>
{#if label !== ""}
<label
for='{inputId()}'
class='wrn-input-number__inner-label'
>
{label}
{#if required}
<span
aria-hidden="true"
class='wrn-input-number__required'
>*</span>
{/if}
</label>
{/if}
{#if description !== ""}
<span
id='{descriptionId()}'
class='wrn-input-number__inner-description'
>
{description}
</span>
{/if}
</div>
{/if}
<div
class='wrn-input-number__field'
>
{#if prefix !== ""}
<span
aria-hidden="true"
class='wrn-input-number__prefix'
>
{prefix}
</span>
{/if}
<input
id='{inputId()}'
name='{name}'
type="number"
value='{currentValue}'
min='{min}'
max='{max}'
step='{normalizedStep()}'
placeholder='{placeholder}'
autocomplete='{autocomplete}'
inputmode='{inputMode}'
aria-label='{ariaLabel !== "" ? ariaLabel : label !== "" ? label : name}'
aria-describedby='{describedBy()}'
aria-invalid='{isInvalid() ? "true" : "false"}'
aria-required='{required ? "true" : "false"}'
aria-disabled='{disabled || inputDisabled ? "true" : "false"}'
required='{required}'
disabled='{disabled}'
readonly='{readonly || inputDisabled || !allowInput}'
tabindex='{inputDisabled ? "-1" : "0"}'
@input='handleInput(event)'
@change='handleChange(event)'
@keydown='handleKeydown(event)'
@wheel='handleWheel(event)'
class='wrn-input-number__input'
/>
{#if suffix !== ""}
<span
aria-hidden="true"
class='wrn-input-number__suffix'
>
{suffix}
</span>
{/if}
</div>
</div>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='wrn-input-number__button wrn-input-number__button--end'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] wrn-input-number__icon'
></span>
</button>
{:else}
{#if showButtons}
<div
role="group"
aria-label='{controlsLabel}'
class='wrn-input-number__buttons'
>
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='wrn-input-number__button wrn-input-number__button--decrement'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] wrn-input-number__icon'
></span>
</button>
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='wrn-input-number__button wrn-input-number__button--increment'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] wrn-input-number__icon'
></span>
</button>
</div>
{/if}
{/if}
</div>
{#if showValidationMessage && isInvalid() && validationMessage() !== ""}
<p
id='{messageId()}'
role="alert"
aria-live="polite"
class='wrn-input-number__message wrn-input-number__message--error'
>
<span
aria-hidden="true"
class='icon-[lucide--circle-alert] wrn-input-number__error-icon'
></span>
<span>{validationMessage()}</span>
</p>
{/if}
{#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
<p
id='{messageId()}'
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; }
}
}
-55
View File
@@ -1,55 +0,0 @@
// 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"
class: string = ""
}
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;
}
}
}
-225
View File
@@ -1,225 +0,0 @@
// 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 {
// 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 {
color: string = "primary"
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
{...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;
}
}
}
}
-108
View File
@@ -1,108 +0,0 @@
component LegendIndicator {
outputs {
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Legend Indicator"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="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="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;
}
}
}
-137
View File
@@ -1,137 +0,0 @@
// 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"
color: string = "primary"
label: string = "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}'
data-size='{size}'
data-color='{color}'
data-underline='{underline}'
data-external='{external}'
>
<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;
}
}
}
-128
View File
@@ -1,128 +0,0 @@
component List {
outputs {
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "List"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section
data-ui-component="List"
aria-label='{title}'
class='wrn-list {class}'
>
{#if title || description}
<header class="wrn-list__header">
{#if title}
<h3 class="wrn-list__title">{title}</h3>
{/if}
{#if description}
<p class="wrn-list__description">{description}</p>
{/if}
</header>
{/if}
<ul
class="wrn-list__items"
data-variant='{variant}'
>
{#each items as item, index}
<li
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="wrn-list__row"
data-size='{size}'
@click='output.select({ item: item, index: index })'
>
{#if item.icon}
<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="wrn-list__image" loading="lazy" />
{/if}
<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="wrn-list__meta">{item.meta}</span>
{/if}
</div>
{#if item.description}
<p class="wrn-list__item-description">{item.description}</p>
{/if}
{#if item.badge}
<span class="wrn-list__badge">{item.badge}</span>
{/if}
</div>
<span class="icon-[lucide--chevron-right] wrn-list__chevron" aria-hidden="true"></span>
</a>
{:else}
<div class="wrn-list__row" data-size='{size}'>
{#if item.icon}
<span class="wrn-list__leading-icon"><span class='{item.icon}' aria-hidden="true"></span>
</span>
{/if}
<div class="wrn-list__body">
<p class="wrn-list__item-title">{item.title || item.label}</p>
{#if item.description}
<p class="wrn-list__item-description">{item.description}</p>
{/if}
</div>
</div>
{/if}
</li>
{:empty}
<li class="wrn-list__empty">
No items available.
</li>
{/each}
</ul>
<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; }
}
}
-108
View File
@@ -1,108 +0,0 @@
component ListGroup {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "List Group"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="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="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;
}
}
}
-111
View File
@@ -1,111 +0,0 @@
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"
title: string = "Map"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section
data-ui-component="Map"
aria-label='{title}'
class='wrn-map {class}'
>
{#if title || description}
<header class="wrn-map__header">
<div>
{#if title}
<h3 class="wrn-map__title">{title}</h3>
{/if}
{#if description}
<p class="wrn-map__description">{description}</p>
{/if}
</div>
<span class="wrn-map__header-icon"><span class="icon-[lucide--map]" aria-hidden="true"></span>
</span>
</header>
{/if}
<div
class="wrn-map__viewport"
data-size='{size}'
>
<div
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="wrn-map__content">
<slot></slot>
</div>
{#if items.length > 0}
{#each items as item, index}
<button
type="button"
aria-label='{item.label || item.title || "Map marker"}'
class="wrn-map__marker"
style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
@click='output.markerClick(item); output.select(item)'
>
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
</button>
{/each}
{/if}
<div class="wrn-map__controls">
<button
type="button"
aria-label="Zoom in"
class="wrn-map__control"
@click='output.zoom({ direction: "in" })'
>
<span class="icon-[lucide--plus] wrn-map__control-icon" aria-hidden="true"></span>
</button>
<button
type="button"
aria-label="Zoom out"
class="wrn-map__control"
@click='output.zoom({ direction: "out" })'
>
<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; }
}
}
-134
View File
@@ -1,134 +0,0 @@
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"
title: string = "Marquee"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
state paused: boolean = false
view {
<section
data-ui-component="Marquee"
aria-label='{title}'
class='wrn-marquee {class}'
>
<div class="wrn-marquee__layout">
{#if title}
<div class="wrn-marquee__title">
<span class="icon-[lucide--megaphone] wrn-marquee__icon" aria-hidden="true"></span>
<span>{title}</span>
</div>
{/if}
<div
class="wrn-marquee__window"
@mouseenter='paused = true; output.pause({})'
@mouseleave='paused = false; output.resume({})'
@focusin='paused = true'
@focusout='paused = false'
>
<div
class="wrn-marquee__track"
class:wrn-marquee-paused='paused'
>
{#each items as item}
<a
href='{item.href || "#"}'
class="wrn-marquee__item"
>
<span
class="wrn-marquee__dot"
data-color='{item.color || "primary"}'
aria-hidden="true"
></span>
<span>{item.label || item.title}</span>
{#if item.meta}
<span class="wrn-marquee__meta">{item.meta}</span>
{/if}
</a>
{:empty}
<p class="wrn-marquee__empty">{description || "No announcements available."}</p>
{/each}
{#if items.length > 0}
{#each items as item}
<a
href='{item.href || "#"}'
aria-hidden="true"
tabindex="-1"
class="wrn-marquee__item"
>
<span class="wrn-marquee__dot" aria-hidden="true"></span>
<span>{item.label || item.title}</span>
</a>
{/each}
{/if}
</div>
</div>
<button
type="button"
aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
class="wrn-marquee__toggle"
@click='paused = !paused; paused ? output.pause({}) : output.resume({})'
>
<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>
<slot></slot>
</section>
}
style {
.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; } }
.wrn-marquee-paused {
animation-play-state: paused;
}
@keyframes wrn-marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.wrn-marquee__track {
animation: none;
}
}
}
}
-344
View File
@@ -1,344 +0,0 @@
// 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 })
close(payload: { reason: string })
select(payload: { item: object; value: string })
}
props {
color: string = "primary"
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 {
<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">&#8250;</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;
}
}
}
}
-787
View File
@@ -1,787 +0,0 @@
component Modal {
outputs {
open(payload: { sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
cancel(payload: { sourceEvent: Event })
confirm(payload: { sourceEvent: Event })
}
props {
open: boolean = false
defaultOpen: boolean = false
title: string = "Modal"
description: string = ""
icon: string = ""
label: string = "Modal dialog"
size: string = "md"
placement: string = "center"
color: string = "primary"
variant: string = "default"
showClose: boolean = true
closeLabel: string = "Close modal"
closeOnBackdrop: boolean = true
closeOnEscape: boolean = true
closeOnCancel: boolean = true
closeOnConfirm: boolean = false
showFooter: boolean = true
cancelLabel: string = "Cancel"
cancelIcon: string = ""
confirmLabel: string = "Confirm"
confirmIcon: string = ""
confirmDisabled: boolean = false
confirmLoading: boolean = false
destructive: boolean = false
triggerLabel: string = ""
triggerIcon: string = ""
scrollable: boolean = true
scrollBehavior: string = "inside"
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showModal(sourceEvent) {
visible = true
output.open({ sourceEvent: sourceEvent })
}
// Slot content can close the modal it sits in by dispatching a bubbling
// wrnexus:modal:close event, e.g. from a form success handler:
//
// event.target.dispatchEvent(
// new CustomEvent("wrnexus:modal:close", { bubbles: true })
// )
//
// The listener is on the modal root, so the event only ever closes the
// 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
output.close({
reason: reason,
sourceEvent: sourceEvent
})
}
client function cancelModal(sourceEvent) {
output.cancel({ sourceEvent: sourceEvent })
if (closeOnCancel) {
hideModal("cancel", sourceEvent)
}
}
client function confirmModal(sourceEvent) {
if (confirmDisabled || confirmLoading) {
return
}
output.confirm({ sourceEvent: sourceEvent })
if (closeOnConfirm) {
hideModal("confirm", sourceEvent)
}
}
client function handleKeydown(sourceEvent) {
if (closeOnEscape && sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
cancelModal(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-ui-component="Modal"
data-open='{isOpen() ? "true" : "false"}'
data-size='{size}'
data-placement='{placement}'
data-color='{color}'
data-variant='{variant}'
data-scrollable='{scrollable ? "true" : "false"}'
data-scroll='{scrollBehavior}'
data-destructive='{destructive ? "true" : "false"}'
class='wrn-modal {class}'
@wrnexus:modal:close='hideModal("api", event)'
>
{#if triggerLabel}
<button
type="button"
class="wrn-modal__trigger"
aria-haspopup="dialog"
aria-expanded='{isOpen() ? "true" : "false"}'
@click='showModal(event)'
>
{#if triggerIcon}
<span
class='{triggerIcon}'
aria-hidden="true"
>
</span>
{/if}
<span>{triggerLabel}</span>
</button>
{/if}
<span
class="wrn-modal__trigger-slot"
@click='showModal(event)'
>
<slot
name="trigger"
>
</slot>
</span>
<div
class="wrn-modal__layer"
data-show="isOpen()"
role="presentation"
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wrn-modal__backdrop"
aria-label='{closeLabel}'
@click='if (closeOnBackdrop) { cancelModal(event) }'
>
</button>
<section
class="wrn-modal__panel"
role="dialog"
aria-modal="true"
aria-label='{label || title}'
tabindex="-1"
>
<header
class="wrn-modal__header"
>
<div
class="wrn-modal__heading"
>
{#if icon}
<span
class='wrn-modal__icon {icon}'
aria-hidden="true"
>
</span>
{/if}
<div
class="wrn-modal__heading-copy"
>
<slot
name="header"
>
</slot>
{#if title}
<h2>{title}</h2>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
</div>
{#if showClose}
<button
type="button"
class="wrn-modal__close"
aria-label='{closeLabel}'
@click='hideModal("close-button", event)'
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18" />
<path d="M6 6 18 18" />
</svg>
</button>
{/if}
</header>
<div
class="wrn-modal__body"
>
<slot></slot>
</div>
{#if showFooter}
<footer
class="wrn-modal__footer"
>
<div
class="wrn-modal__custom-footer"
>
<slot
name="footer"
>
</slot>
</div>
<div
class="wrn-modal__actions"
>
{#if cancelLabel}
<button
type="button"
class="wrn-modal__button wrn-modal__button--secondary"
@click='cancelModal(event)'
>
{#if cancelIcon}
<span
class='{cancelIcon}'
aria-hidden="true"
>
</span>
{/if}
<span>{cancelLabel}</span>
</button>
{/if}
{#if confirmLabel}
<button
type="button"
class="wrn-modal__button wrn-modal__button--primary"
disabled='{confirmDisabled || confirmLoading}'
@click='confirmModal(event)'
>
{#if confirmLoading}
<span
class="wrn-modal__spinner"
aria-hidden="true"
>
</span>
{:else if confirmIcon}
<span
class='{confirmIcon}'
aria-hidden="true"
>
</span>
{/if}
<span>{confirmLabel}</span>
</button>
{/if}
</div>
</footer>
{/if}
</section>
</div>
</div>
}
style {
.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;
}
.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(--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.
*/
.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);
}
.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);
}
.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);
}
.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);
}
.wrn-modal__trigger,
.wrn-modal__trigger-slot {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.wrn-modal__trigger {
appearance: none;
min-height: 2.55rem;
padding: 0.65rem 1rem;
color: var(--modal-contrast);
background: var(--modal-accent);
border: 0;
border-radius: 0.8rem;
font: inherit;
font-size: 0.85rem;
font-weight: 650;
cursor: pointer;
transition: opacity 150ms ease, transform 150ms ease, box-shadow 150ms ease;
}
.wrn-modal__trigger:hover {
opacity: 0.92;
}
.wrn-modal__trigger:active {
transform: scale(0.97);
}
.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 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
* client JS having run yet. The data-show attribute and client directive
* still run after hydration to keep things in sync for state changes,
* but visibility itself is driven by CSS here. visibility (rather than
* display) is used so the open and close transitions below can actually
* animate -- a box that starts at display: none has no prior frame to
* transition from.
*/
.wrn-modal__layer {
position: fixed;
inset: 0;
z-index: 1250;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
visibility: hidden;
opacity: 0;
transition: opacity 180ms ease, visibility 0s linear 180ms;
}
.wrn-modal[data-open="true"] .wrn-modal__layer {
visibility: visible;
opacity: 1;
transition: opacity 180ms ease, visibility 0s linear 0s;
}
.wrn-modal[data-placement="top"] .wrn-modal__layer {
align-items: flex-start;
padding-top: clamp(1rem, 8vh, 5rem);
}
.wrn-modal[data-scroll="page"] .wrn-modal__layer {
align-items: flex-start;
overflow-y: auto;
padding: 2.5rem 1rem;
}
.wrn-modal__backdrop {
position: absolute;
inset: 0;
z-index: 0;
appearance: none;
padding: 0;
background: color-mix(in srgb, black 62%, transparent);
border: 0;
backdrop-filter: blur(9px);
}
.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(--wrn-color-text);
background:
radial-gradient(
circle at 100% 0%,
color-mix(in srgb, var(--modal-accent) 9%, transparent),
transparent 34%
),
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,
0 40px 110px color-mix(in srgb, black 38%, transparent);
overflow: hidden;
opacity: 0;
transform: scale(0.96) translateY(10px);
transition: opacity 180ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.wrn-modal[data-open="true"] .wrn-modal__panel {
opacity: 1;
transform: none;
}
.wrn-modal[data-size="xs"] .wrn-modal__panel {
width: min(19rem, calc(100vw - 2rem));
}
.wrn-modal[data-size="sm"] .wrn-modal__panel {
width: min(25rem, calc(100vw - 2rem));
}
.wrn-modal[data-size="lg"] .wrn-modal__panel {
width: min(44rem, calc(100vw - 2rem));
}
.wrn-modal[data-size="xl"] .wrn-modal__panel {
width: min(64rem, calc(100vw - 2rem));
}
.wrn-modal[data-size="full"] .wrn-modal__panel {
width: calc(100vw - 2rem);
height: calc(100vh - 2rem);
max-height: none;
}
.wrn-modal[data-scroll="page"] .wrn-modal__panel {
max-height: none;
}
.wrn-modal[data-variant="soft"] .wrn-modal__panel {
background:
linear-gradient(145deg, var(--modal-soft), transparent 68%),
var(--wrn-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);
}
.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);
}
.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(--wrn-color-border);
}
.wrn-modal__heading {
display: flex;
align-items: flex-start;
gap: 0.9rem;
min-width: 0;
}
.wrn-modal__icon {
flex: 0 0 auto;
width: 1.3rem;
height: 1.3rem;
margin-top: 0.15rem;
color: var(--modal-accent);
}
.wrn-modal[data-variant="solid"] .wrn-modal__icon {
color: currentColor;
}
.wrn-modal__heading-copy {
display: grid;
gap: 0.3rem;
min-width: 0;
}
.wrn-modal__heading-copy h2,
.wrn-modal__heading-copy p {
margin: 0;
}
.wrn-modal__heading-copy h2 {
color: inherit;
font-size: 1.08rem;
font-weight: 650;
line-height: 1.3;
}
.wrn-modal__heading-copy p {
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
line-height: 1.55;
}
.wrn-modal[data-variant="solid"] .wrn-modal__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
/*
* padding is reset explicitly: an app-level `button { padding: … }` rule
* outranks the browser default, and 1rem of horizontal padding left this
* 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".
*/
.wrn-modal__close {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0;
width: 2.15rem;
height: 2.15rem;
color: var(--wrn-color-text-muted);
background: transparent;
border: 1px solid transparent;
border-radius: 9999px;
cursor: pointer;
transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms ease;
}
/*
* On a solid panel the background is the accent color, so the muted-grey
* default is close to invisible -- the dismiss affordance reads as
* missing rather than subtle. Derive it from the panel contrast color
* instead, and give it a faint ring so it is unmistakably a control.
*/
.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);
}
.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);
}
.wrn-modal__close:hover {
color: var(--wrn-color-text);
background: var(--wrn-color-surface-soft);
}
.wrn-modal__close:focus-visible {
color: var(--modal-accent);
border-color: color-mix(in srgb, var(--modal-accent) 45%, transparent);
outline: none;
}
.wrn-modal__close:active {
transform: scale(0.92);
}
/* Never let the glyph be shrunk by the flex container. */
.wrn-modal__close svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
}
/*
* Slot content is authored by the host app, so the app global stylesheet
* styles it too. A bare element selector there (p { color: ... }) beats
* anything the panel merely *inherits*, which is how solid-variant modals
* ended up with muted grey body copy on a saturated accent background --
* unreadable, and worst exactly where contrast matters most (the
* destructive confirm). Setting the color on the body makes the panel
* choice explicit instead of leaving it to inheritance.
*
* NOTE: apostrophes are avoided in .wrn style comments on purpose -- the
* block scanner treats a quote as a string delimiter while it counts
* braces, so a stray one breaks parsing of the whole component.
*/
.wrn-modal__body {
flex: 1 1 auto;
min-height: 0;
padding: 1.4rem;
color: var(--wrn-color-text);
}
/*
* :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 .wrn-modal__body p list would have quietly overridden those.
*/
.wrn-modal__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
color: inherit;
}
.wrn-modal[data-variant="solid"] .wrn-modal__body {
color: var(--modal-contrast);
}
.wrn-modal[data-scrollable="true"] .wrn-modal__body {
overflow: auto;
overscroll-behavior: contain;
}
.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(--wrn-color-border);
}
.wrn-modal__custom-footer:empty {
display: none;
}
.wrn-modal__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.65rem;
margin-left: auto;
}
.wrn-modal__button {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 2.55rem;
padding: 0.65rem 1rem;
border-radius: 0.78rem;
font: inherit;
font-size: 0.83rem;
font-weight: 650;
cursor: pointer;
transition: transform 150ms ease, opacity 150ms ease, border-color 150ms ease;
}
.wrn-modal__button:active {
transform: scale(0.985);
}
.wrn-modal__button--secondary {
color: var(--wrn-color-text);
background: transparent;
border: 1px solid var(--wrn-color-border);
}
.wrn-modal__button--primary {
color: var(--modal-contrast);
background: var(--modal-accent);
border: 1px solid transparent;
}
/*
* On a solid-variant panel the panel background is also --modal-accent,
* so a plain primary button (same color) has no visible edge against it.
* 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.
*/
.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;
}
.wrn-modal__button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.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: wrn-modal-spin 750ms linear infinite;
}
@keyframes wrn-modal-spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 639px) {
.wrn-modal__layer {
align-items: flex-end;
padding: 0;
}
.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;
}
.wrn-modal__footer {
align-items: stretch;
flex-direction: column;
}
.wrn-modal__actions {
width: 100%;
}
.wrn-modal__button {
flex: 1 1 0;
}
}
@media (prefers-reduced-motion: reduce) {
.wrn-modal__button,
.wrn-modal__spinner,
.wrn-modal__layer,
.wrn-modal__panel,
.wrn-modal__close {
animation: none;
transition: none;
}
.wrn-modal__panel {
transform: none;
}
}
}
}
-354
View File
@@ -1,354 +0,0 @@
// 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: { item: object; value: 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 = ""
}
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
{...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"
>&#8250;</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"
>&#8250;</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;
}
}
}
}
-648
View File
@@ -1,648 +0,0 @@
component Navbar {
outputs {
toggle(payload: { open: boolean })
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { item: string | number | boolean | null | object; value: string | number | boolean; level: string })
action(payload: { item: string | number | boolean | null | object; value: string | number | boolean })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Primary navigation"
topbarLabel: string = "Utility navigation"
brand: Record<string, unknown> = {}
items: unknown[] = []
actions: unknown[] = []
active: string = ""
sticky: boolean = false
openOnHover: boolean = false
maxWidth: string = "full"
mobileLabel: string = "Toggle navigation"
class: string = ""
}
state mobileOpen: boolean = false
functions {
client function toggleNavigation() {
mobileOpen = !mobileOpen
output.toggle({ open: mobileOpen })
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
}
client function selectItem(item, level) {
mobileOpen = false
output.select({ item: item, value: item.value || "", level: level })
}
client function selectAction(item) {
mobileOpen = false
output.action({ item: item, value: item.value || "" })
}
shared function isItemActive(item) {
if (!item) return false
if (item.value && item.value === active) return true
if (!item.children || !item.children.length) return false
return item.children.some(function (child) {
return isItemActive(child)
})
}
client function toggleDropdown(event, item) {
output[event.currentTarget.open ? "open" : "close"]({
source: "menu",
item: item
})
}
}
view {
<header class="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="wrn-navbar__main">
<a class="wrn-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
{#if brand.logo}
<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="wrn-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
{/if}
{#if brand.label || brand.description}
<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="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="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="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="wrn-navbar__chevron" aria-hidden="true"></span>
</summary>
<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="wrn-navbar__group">
{#if child.children && child.children.length}
{#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' : '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' : '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>
{/if}
</div>
{/each}
</div>
</details>
{:else}
<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>
{/if}
{/each}
</nav>
<div class="wrn-navbar__actions">
{#each actions as 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>
{/each}
<slot name="actions" />
</div>
</div>
</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;
}
}
}
-291
View File
@@ -1,291 +0,0 @@
// 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: { page: number; pageSize: number })
previous(payload: { page: number })
next(payload: { page: number })
}
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 = ""
}
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
{...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">&#8249;</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">&#8250;</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;
}
}
}
}
-307
View File
@@ -1,307 +0,0 @@
component PinInput {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
paste(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Verification code"
name: string = "pin"
value: string = ""
length: number = 4
pattern: string = "[0-9]"
type: string = "text"
inputMode: string = "numeric"
placeholder: string = "○"
autocomplete: string = "one-time-code"
masked: boolean = false
disabled: boolean = false
readonly: boolean = false
required: boolean = false
autoFocus: boolean = false
autoSubmit: boolean = false
allowPaste: boolean = true
clearable: boolean = true
clearLabel: string = "Clear code"
separator: string = ""
groupSize: number = 0
helpText: string = ""
invalid: boolean = false
validationMessage: string = ""
class: string = ""
}
functions {
shared function inputIndexes() {
return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].slice(
0,
Math.max(1, Math.min(12, Number(length)))
)
}
}
view {
<fieldset
{...attrs}
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--pin-input {invalid ? 'wrn-next--invalid' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
data-wrn-pin-input
data-length="{length}"
data-pattern="{pattern}"
data-allow-paste="{allowPaste ? 'true' : 'false'}"
data-auto-submit="{autoSubmit ? 'true' : 'false'}"
data-value="{value}"
disabled="{disabled}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
>
<div class="wrn-next__pin-heading">
<legend>{label}</legend>
<button
type="button"
class="wrn-next__pin-clear"
data-pin-clear
aria-label="{clearLabel}"
title="{clearLabel}"
style="{clearable && value ? '' : 'display: none'}"
>
<span class="icon-[lucide--rotate-ccw]" aria-hidden="true"></span>
<span>{clearLabel}</span>
</button>
</div>
<div class="wrn-next__pin-cells" role="group" aria-label="{label}">
{#each inputIndexes() as index}
<input
data-pin-cell
data-pin-index="{index}"
id="{name}-{index}"
type="{masked ? 'password' : type}"
inputmode="{inputMode}"
maxlength="1"
value="{value[index] || ''}"
placeholder="{placeholder}"
autocomplete="{index === 0 ? autocomplete : 'off'}"
aria-label="{label} digit {index + 1} of {length}"
aria-required="{required ? 'true' : 'false'}"
readonly="{readonly}"
disabled="{disabled}"
autofocus="{autoFocus && index === 0}"
/>
{#if separator && groupSize !== 0 && (index + 1) % groupSize === 0 && index + 1 !== length}
<span class="wrn-next__pin-separator" aria-hidden="true">{separator}</span>
{/if}
{/each}
</div>
<input
type="hidden"
name="{name}"
value="{value}"
required="{required}"
data-pin-value
/>
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wrn-next__validation"
data-error="{name}"
>{validationMessage}</small>
</fieldset>
}
style {
.wrn-next--pin-input.wrn-next--invalid .wrn-next__pin-cells input,
.wrn-next--pin-input:has(input.wrn-invalid) .wrn-next__pin-cells input {
border-color: var(--wrn-color-danger);
}
.wrn-next--pin-input {
display: grid;
width: 100%;
min-width: 0;
gap: 0.65rem;
padding: 0;
border: 0;
color: var(--wrn-color-text);
}
.wrn-next__pin-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__pin-heading legend {
padding: 0;
font-size: 0.82em;
font-weight: 700;
}
.wrn-next__pin-clear {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0;
border: 0;
color: var(--wrn-component-color);
background: transparent;
font: inherit;
font-size: 0.7em;
font-weight: 750;
cursor: pointer;
}
.wrn-next__pin-clear > span:first-child {
width: 0.9rem;
height: 0.9rem;
}
.wrn-next__pin-cells {
display: flex;
max-width: 100%;
align-items: center;
gap: clamp(0.35rem, 1.5vw, 0.65rem);
overflow-x: auto;
padding: 0.2rem;
scrollbar-width: thin;
}
.wrn-next--pin-input .wrn-next__pin-cells input {
flex: 0 0 clamp(2.4rem, 8vw, 3.25rem);
width: clamp(2.4rem, 8vw, 3.25rem);
height: clamp(2.75rem, 9vw, 3.5rem);
min-width: clamp(2.4rem, 8vw, 3.25rem);
padding: 0;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font: inherit;
font-size: clamp(1rem, 3vw, 1.25rem);
font-weight: 750;
text-align: center;
caret-color: var(--wrn-component-color);
transition:
border-color var(--wrn-motion-base) var(--wrn-ease-standard),
box-shadow var(--wrn-motion-base) var(--wrn-ease-standard),
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next--pin-input .wrn-next__pin-cells input::placeholder {
color: var(--wrn-color-muted);
opacity: 0.45;
}
.wrn-next--pin-input .wrn-next__pin-cells input:focus-visible {
border-color: var(--wrn-component-color);
outline: 0;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 18%, transparent);
transform: translateY(-1px);
}
.wrn-next--pin-input .wrn-next__pin-cells input:disabled,
.wrn-next--pin-input .wrn-next__pin-cells input:read-only {
cursor: not-allowed;
opacity: 0.55;
}
.wrn-next__pin-separator {
color: var(--wrn-color-muted);
font-size: 1.1rem;
font-weight: 800;
}
.wrn-next--pin-input[data-complete="true"] .wrn-next__pin-cells input {
border-color: color-mix(in srgb, var(--wrn-component-color) 72%, var(--wrn-color-border));
background: color-mix(in srgb, var(--wrn-component-color) 7%, var(--wrn-color-bg));
}
@media (max-width: 480px) {
.wrn-next__pin-cells {
gap: 0.3rem;
}
.wrn-next--pin-input .wrn-next__pin-cells input {
flex-basis: min(2.7rem, 12vw);
width: min(2.7rem, 12vw);
min-width: min(2.7rem, 12vw);
height: min(3rem, 14vw);
}
}
/* 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;
}
}
}
-543
View File
@@ -1,543 +0,0 @@
component Popover {
outputs {
toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
open(payload: { sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
action(payload: { href: string; sourceEvent: Event })
}
props {
open: boolean = false
defaultOpen: boolean = false
triggerLabel: string = "Open popover"
triggerIcon: string = ""
title: string = ""
description: string = ""
icon: string = ""
placement: string = "bottom-start"
width: string = "md"
size: string = "default"
color: string = "primary"
variant: string = "raised"
showArrow: boolean = true
showClose: boolean = false
closeLabel: string = "Close popover"
closeOnOutside: boolean = true
closeOnEscape: boolean = true
actionLabel: string = ""
actionHref: string = ""
actionIcon: string = ""
closeOnAction: boolean = true
disabled: boolean = false
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showPopover(sourceEvent) {
if (disabled) {
return
}
visible = true
output.open({ sourceEvent: sourceEvent })
output.toggle({ open: true, sourceEvent: sourceEvent })
}
client function hidePopover(reason, sourceEvent) {
visible = false
output.close({ reason: reason, sourceEvent: sourceEvent })
output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
}
client function togglePopover(sourceEvent) {
if (isOpen()) {
hidePopover("toggle", sourceEvent)
} else {
showPopover(sourceEvent)
}
}
client function activateAction(sourceEvent) {
output.action({
href: actionHref,
sourceEvent: sourceEvent
})
if (closeOnAction) {
hidePopover("action", sourceEvent)
}
}
client function handleKeydown(sourceEvent) {
if (closeOnEscape && sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hidePopover("escape", sourceEvent)
}
}
}
view {
<div
{...attrs}
data-ui-component="Popover"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-width='{width}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wrn-popover {class}'
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wrn-popover__trigger"
disabled='{disabled}'
aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}'
@click='togglePopover(event)'
>
<slot name="trigger"></slot>
{#if triggerIcon}
<span class='{triggerIcon}' aria-hidden="true"></span>
{/if}
{#if triggerLabel}
<span>{triggerLabel}</span>
{/if}
</button>
{#if closeOnOutside}
<button
type="button"
class="wrn-popover__dismiss-layer"
data-show='{open || visible}'
aria-label='{closeLabel}'
@click='hidePopover("outside", event)'
></button>
{/if}
<section
class="wrn-popover__panel"
data-wrn-anchored="true"
data-show='{open || visible}'
role="dialog"
aria-label='{title || triggerLabel}'
>
{#if showArrow}
<span class="wrn-popover__arrow" data-wrn-anchor-arrow="true" aria-hidden="true"></span>
{/if}
{#if title || description || icon || showClose}
<header class="wrn-popover__header">
<div class="wrn-popover__heading">
{#if icon}
<span class='wrn-popover__icon {icon}' aria-hidden="true"></span>
{/if}
<div class="wrn-popover__heading-copy">
<slot name="header"></slot>
{#if title}
<h3>{title}</h3>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
</div>
{#if showClose}
<button
type="button"
class="wrn-popover__close"
aria-label='{closeLabel}'
@click='hidePopover("close-button", event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</header>
{/if}
<div class="wrn-popover__body">
<slot></slot>
</div>
{#if actionLabel}
<footer class="wrn-popover__footer">
{#if actionHref}
<a
href='{actionHref}'
class="wrn-popover__action"
@click='activateAction(event)'
>
<span>{actionLabel}</span>
{#if actionIcon}
<span class='{actionIcon}' aria-hidden="true"></span>
{:else}
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
class="wrn-popover__action"
@click='activateAction(event)'
>
<span>{actionLabel}</span>
{#if actionIcon}
<span class='{actionIcon}' aria-hidden="true"></span>
{:else}
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
{/if}
</button>
{/if}
</footer>
{/if}
<slot name="footer"></slot>
</section>
</div>
}
style {
.wrn-popover {
--popover-accent: var(--wrn-color-primary);
--popover-soft: var(--wrn-color-primary-soft);
--popover-contrast: var(--wrn-color-primary-contrast);
position: relative;
display: inline-flex;
}
.wrn-popover[data-color="secondary"] {
--popover-accent: var(--wrn-color-secondary);
--popover-soft: var(--wrn-color-secondary-soft);
--popover-contrast: var(--wrn-color-secondary-contrast);
}
.wrn-popover[data-color="info"] {
--popover-accent: var(--wrn-color-info);
--popover-soft: var(--wrn-color-info-soft);
--popover-contrast: var(--wrn-color-info-contrast);
}
.wrn-popover[data-color="success"] {
--popover-accent: var(--wrn-color-success);
--popover-soft: var(--wrn-color-success-soft);
--popover-contrast: var(--wrn-color-success-contrast);
}
.wrn-popover[data-color="warning"] {
--popover-accent: var(--wrn-color-warning);
--popover-soft: var(--wrn-color-warning-soft);
--popover-contrast: var(--wrn-color-warning-text);
}
.wrn-popover[data-color="danger"] {
--popover-accent: var(--wrn-color-danger);
--popover-soft: var(--wrn-color-danger-soft);
--popover-contrast: var(--wrn-color-on-danger);
}
.wrn-popover__trigger {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.55rem;
min-height: 2.55rem;
padding: 0.65rem 0.9rem;
color: var(--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;
font-weight: 600;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
}
.wrn-popover__trigger:hover,
.wrn-popover__trigger:focus-visible,
.wrn-popover[data-open="true"] .wrn-popover__trigger {
color: var(--popover-accent);
background: var(--popover-soft);
border-color: color-mix(in srgb, var(--popover-accent) 38%, var(--wrn-color-border));
outline: none;
}
.wrn-popover__trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.wrn-popover[data-size="sm"] .wrn-popover__trigger {
min-height: 2.2rem;
padding: 0.5rem 0.72rem;
font-size: 0.78rem;
}
.wrn-popover[data-size="lg"] .wrn-popover__trigger {
min-height: 2.9rem;
padding: 0.78rem 1.05rem;
font-size: 0.9rem;
}
.wrn-popover__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1120;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wrn-popover__panel {
position: absolute;
z-index: 1121;
top: calc(100% + 0.72rem);
left: 0;
width: 18rem;
max-width: calc(100vw - 1.5rem);
color: var(--wrn-color-text);
background:
linear-gradient(145deg, color-mix(in srgb, var(--popover-accent) 5%, transparent), transparent 62%),
color-mix(in srgb, var(--wrn-color-surface-raised) 97%, transparent);
border: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wrn-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 24px 64px color-mix(in srgb, black 22%, transparent);
backdrop-filter: blur(18px);
}
.wrn-popover[data-width="sm"] .wrn-popover__panel {
width: 14rem;
}
.wrn-popover[data-width="lg"] .wrn-popover__panel {
width: 24rem;
}
.wrn-popover[data-width="xl"] .wrn-popover__panel {
width: 32rem;
}
.wrn-popover[data-width="trigger"] .wrn-popover__panel {
width: 100%;
min-width: 100%;
}
.wrn-popover[data-placement="bottom-end"] .wrn-popover__panel {
right: 0;
left: auto;
}
.wrn-popover[data-placement="top-start"] .wrn-popover__panel {
top: auto;
bottom: calc(100% + 0.72rem);
}
.wrn-popover[data-placement="top-end"] .wrn-popover__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.72rem);
left: auto;
}
.wrn-popover[data-placement="left"] .wrn-popover__panel {
top: 50%;
right: calc(100% + 0.72rem);
left: auto;
transform: translateY(-50%);
}
.wrn-popover[data-placement="right"] .wrn-popover__panel {
top: 50%;
left: calc(100% + 0.72rem);
transform: translateY(-50%);
}
.wrn-popover[data-variant="soft"] .wrn-popover__panel {
background:
linear-gradient(145deg, var(--popover-soft), transparent 70%),
var(--wrn-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wrn-popover[data-variant="outline"] .wrn-popover__panel {
background: var(--wrn-color-surface-raised);
box-shadow: none;
}
.wrn-popover[data-variant="solid"] .wrn-popover__panel {
color: var(--popover-contrast);
background: var(--popover-accent);
border-color: color-mix(in srgb, white 20%, transparent);
}
.wrn-popover__arrow {
position: absolute;
top: -0.35rem;
left: 1.25rem;
width: 0.7rem;
height: 0.7rem;
background: inherit;
border-top: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wrn-color-border));
border-left: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wrn-color-border));
transform: rotate(45deg);
}
.wrn-popover[data-placement="bottom-end"] .wrn-popover__arrow {
right: 1.25rem;
left: auto;
}
.wrn-popover[data-placement="top-start"] .wrn-popover__arrow,
.wrn-popover[data-placement="top-end"] .wrn-popover__arrow {
top: auto;
bottom: -0.35rem;
transform: rotate(225deg);
}
.wrn-popover[data-placement="left"] .wrn-popover__arrow {
top: 50%;
right: -0.35rem;
left: auto;
transform: translateY(-50%) rotate(135deg);
}
.wrn-popover[data-placement="right"] .wrn-popover__arrow {
top: 50%;
left: -0.35rem;
transform: translateY(-50%) rotate(-45deg);
}
.wrn-popover__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.8rem;
padding: 1rem 1rem 0.8rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-popover__heading {
display: flex;
align-items: flex-start;
gap: 0.7rem;
min-width: 0;
}
.wrn-popover__icon {
flex: 0 0 auto;
width: 1.05rem;
height: 1.05rem;
margin-top: 0.15rem;
color: var(--popover-accent);
}
.wrn-popover[data-variant="solid"] .wrn-popover__icon {
color: currentColor;
}
.wrn-popover__heading-copy {
display: grid;
gap: 0.2rem;
min-width: 0;
}
.wrn-popover__heading-copy h3,
.wrn-popover__heading-copy p {
margin: 0;
}
.wrn-popover__heading-copy h3 {
font-size: 0.9rem;
font-weight: 650;
}
.wrn-popover__heading-copy p {
color: var(--wrn-color-text-muted);
font-size: 0.74rem;
line-height: 1.5;
}
.wrn-popover[data-variant="solid"] .wrn-popover__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
/*
* padding is reset explicitly: an app-level `button { padding: ... }` rule
* outranks the browser default and crushes the icon inside this
* fixed-size button. Same trap as Modal and Drawer.
*/
.wrn-popover__close {
padding: 0;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 2rem;
height: 2rem;
color: var(--wrn-color-text-muted);
background: var(--wrn-color-surface-soft);
border: 1px solid var(--wrn-color-border);
border-radius: 0.65rem;
cursor: pointer;
}
.wrn-popover__body {
padding: 1rem;
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
line-height: 1.6;
}
.wrn-popover__footer {
padding: 0 1rem 1rem;
}
.wrn-popover__action {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
min-height: 2.35rem;
padding: 0.58rem 0.8rem;
color: var(--popover-contrast);
background: var(--popover-accent);
border: 0;
border-radius: 0.7rem;
font: inherit;
font-size: 0.78rem;
font-weight: 650;
text-decoration: none;
cursor: pointer;
}
@media (max-width: 639px) {
.wrn-popover__panel {
position: fixed;
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
max-width: none;
transform: none;
}
.wrn-popover__arrow {
display: none;
}
}
}
}
-767
View File
@@ -1,767 +0,0 @@
component RangeSlider {
outputs {
input(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
change(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Range"
hiddenLabel: boolean = false
placeholder: string = ""
variant: string = "normal"
icon: string = ""
iconPosition: string = "start"
value: number = 50
min: number = 0
max: number = 100
step: number = 1
showValue: boolean = true
showBounds: boolean = true
showSteps: boolean = false
marks: unknown[] = []
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state currentValue = value
functions {
shared function percent() { if (Number(max) === Number(min)) { return 0 } return (Number(currentValue) - Number(min)) / (Number(max) - Number(min)) * 100 }
client function setValue(nextValue, sourceEvent, commit, detail, root, slider, input, outputElement, progressValue) { if (readonly || disabled) { return } currentValue = Number(nextValue); root = sourceEvent.currentTarget.closest(".wrn-next--range-slider"); if (root) { slider = root.querySelector("[role=slider]"); input = root.querySelector("input[name='" + name + "']"); outputElement = root.querySelector("output"); progressValue = (currentValue - Number(min)) / (Number(max) - Number(min)) * 100; if (slider) { slider.setAttribute("aria-valuenow", String(currentValue)); slider.style.setProperty("--wrn-range-progress", progressValue + "%") } if (input) { input.value = String(currentValue) } if (outputElement) { outputElement.textContent = String(currentValue) } } detail = { value: currentValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); if (commit) { output.change(detail) } }
client function valueFromPointer(sourceEvent, rect, ratio) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); return Number(min) + ratio * (Number(max) - Number(min)) }
client function beginSlide(sourceEvent, rect, ratio, rawValue) { if (readonly || disabled) { return } sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId); rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function moveSlide(sourceEvent, rect, ratio, rawValue) { if (!sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { return } rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function endSlide(sourceEvent, rect, ratio, rawValue) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); if (sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { sourceEvent.currentTarget.releasePointerCapture(sourceEvent.pointerId) } output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function selectMark(sourceEvent, root, slider, input, outputElement, nextValue, progressValue, detail) { if (readonly || disabled) { return } root = sourceEvent.currentTarget.closest(".wrn-next--range-slider"); slider = root.querySelector(".wrn-next__custom-slider"); input = root.querySelector(".wrn-next__range-value"); outputElement = root.querySelector("output"); nextValue = Number(sourceEvent.currentTarget.dataset.value); currentValue = nextValue; progressValue = (nextValue - Number(min)) / (Number(max) - Number(min)) * 100; slider.setAttribute("aria-valuenow", String(nextValue)); slider.style.setProperty("--wrn-range-progress", progressValue + "%"); input.setAttribute("value", String(nextValue)); outputElement.replaceChildren(String(nextValue)); detail = { value: nextValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function handleKey(sourceEvent) { if (sourceEvent.key === "ArrowRight") { sourceEvent.preventDefault(); currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "ArrowLeft") { sourceEvent.preventDefault(); currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "Home") { sourceEvent.preventDefault(); currentValue = Number(min); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "End") { sourceEvent.preventDefault(); currentValue = Number(max); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } }
client function emitFocus(nameEvent, sourceEvent) { output[nameEvent]({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function decreaseValue(sourceEvent) { currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function increaseValue(sourceEvent) { currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--range-slider {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__range-control" data-variant="{variant}" data-icon-position="{iconPosition}" data-show-steps="{showSteps}">
{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}
<button type="button" class="wrn-next__range-stepper" aria-label="Decrease {label}" disabled="{disabled || readonly}" @click="decreaseValue(event)"></button>
<div class="wrn-next__range-track">
<input id="{id || name}" class="wrn-next__sr-only wrn-next__range-value" type="number" name="{name}" value="{currentValue}" min="{min}" max="{max}" step="{step}" required="{required}" readonly />
<div class="wrn-next__custom-slider" role="slider" tabindex="{disabled ? '-1' : '0'}" aria-label="{hiddenLabel ? label : ''}" aria-valuemin="{min}" aria-valuemax="{max}" aria-valuenow="{currentValue}" aria-disabled="{disabled}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" style="--wrn-range-progress: {percent()}%" @pointerdown="beginSlide(event)" @pointermove="moveSlide(event)" @pointerup="endSlide(event)" @pointercancel="endSlide(event)" @keydown="handleKey(event)" @focus="emitFocus('focus', event)" @blur="emitFocus('blur', event)">
<span class="wrn-next__slider-fill"></span><span class="wrn-next__slider-thumb"></span>
{#if marks.length}<span class="wrn-next__slider-marks">{#each marks as mark}<button type="button" data-value="{mark.value}" style="left: {(Number(mark.value) - Number(min)) / (Number(max) - Number(min)) * 100}%" aria-label="{mark.label}" @click="selectMark(event)"></button>{/each}</span>{/if}
</div>
{#if showBounds}<div class="wrn-next__range-bounds"><span>{min}</span><span>Step {step}</span><span>{max}</span></div>{/if}
</div>
{#if showValue}<output for="{id || name}">{currentValue}</output>{/if}
<button type="button" class="wrn-next__range-stepper" aria-label="Increase {label}" disabled="{disabled || readonly}" @click="increaseValue(event)">+</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__range-track {
display: grid;
flex: 1;
min-width: 0;
gap: 0.35rem;
}
.wrn-next__range-control input {
width: 100%;
min-width: 0;
margin: 0;
flex: 1;
accent-color: var(--wrn-field-color);
}
.wrn-next__custom-slider {
position: relative;
width: 100%;
height: 1.5rem;
cursor: pointer;
touch-action: none;
}
.wrn-next__custom-slider::before,
.wrn-next__slider-fill {
position: absolute;
top: 50%;
height: 0.4rem;
border-radius: 999px;
transform: translateY(-50%);
content: "";
}
.wrn-next__custom-slider::before {
right: 0;
left: 0;
background: var(--wrn-color-surface-2);
box-shadow: inset 0 0 0 1px var(--wrn-color-border);
}
.wrn-next__slider-fill {
z-index: 1;
left: 0;
width: var(--wrn-range-progress);
background: var(--wrn-field-color);
}
.wrn-next__slider-thumb {
position: absolute;
z-index: 3;
top: 50%;
left: var(--wrn-range-progress);
width: 1.25rem;
height: 1.25rem;
border: 3px solid var(--wrn-field-color);
border-radius: 50%;
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
transform: translate(-50%, -50%);
}
.wrn-next__custom-slider:focus-visible {
outline: 3px solid color-mix(in srgb, var(--wrn-field-color) 28%, transparent);
outline-offset: 3px;
}
.wrn-next__slider-marks button {
position: absolute;
z-index: 2;
top: 50%;
width: 0.55rem;
height: 0.55rem;
padding: 0;
border: 2px solid var(--wrn-color-surface);
border-radius: 50%;
background: var(--wrn-color-muted);
transform: translate(-50%, -50%);
}
.wrn-next__range-control[data-show-steps="true"] input {
background-image: repeating-linear-gradient(
90deg,
transparent 0,
transparent calc(10% - 1px),
var(--wrn-color-border) calc(10% - 1px),
var(--wrn-color-border) 10%
);
}
.wrn-next__range-bounds {
display: grid;
grid-template-columns: 1fr auto 1fr;
color: var(--wrn-color-muted);
font-size: 0.78em;
font-variant-numeric: tabular-nums;
}
.wrn-next__range-bounds span:nth-child(2) {
text-align: center;
}
.wrn-next__range-bounds span:last-child {
text-align: right;
}
.wrn-next__range-control output {
min-width: 3ch;
font-variant-numeric: tabular-nums;
}
.wrn-next__range-stepper {
display: inline-grid;
width: 2.35rem;
height: 2.35rem;
flex: 0 0 auto;
place-items: center;
border: 1px solid var(--wrn-color-border);
border-radius: 50%;
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
font-size: 1.2rem;
cursor: pointer;
}
.wrn-next__range-stepper:hover:not(:disabled) {
border-color: var(--wrn-field-color);
color: var(--wrn-field-color);
}
.wrn-next__range-stepper:disabled {
cursor: not-allowed;
opacity: 0.45;
}
/* 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);
}
}
}
-108
View File
@@ -1,108 +0,0 @@
component Rating {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Rating"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--rating wrn-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
style {
.wrn-next--rating {
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--rating > p { margin: 0; color: var(--wrn-color-muted); }
.wrn-next--rating > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-next--rating > .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;
}
}
}
-172
View File
@@ -1,172 +0,0 @@
// Scrollspy -- a table of contents that follows the reader.
//
// <Scrollspy items='[{"label":"Overview","href":"#overview"}]' />
//
// Each href points at an element on the page. The runtime observes those
// elements and moves aria-current to the link for whichever one is in view.
//
// The runtime writes the marker straight onto the links rather than into
// component state. An IntersectionObserver callback fires long after the
// client function that registered it has returned, and a state write made
// there is dropped -- so the DOM is the only place the answer can live.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Scrollspy {
outputs {
change(payload: { href: string; label: string })
}
props {
color: string = "primary"
size: string = "default"
items: unknown[] = []
active: string = ""
label: string = "On this page"
heading: string = ""
class: string = ""
}
functions {
shared function itemList() {
return Array.isArray(items) ? items : []
}
shared function isActive(item) {
return Boolean(item.href) && item.href === active
}
}
view {
<nav
{...attrs}
data-ui-component="Scrollspy"
class='wrn-scrollspy {class}'
data-color='{color}'
data-size='{size}'
data-wrn-scrollspy="true"
role="navigation"
aria-label='{label}'
>
<p class="wrn-scrollspy__heading" data-show="heading">{heading}</p>
<ul class="wrn-scrollspy__list">
{#each itemList() as item}
<li class="wrn-scrollspy__item">
<a
class="wrn-scrollspy__link"
href='{item.href || "#"}'
data-active='{isActive(item)}'
aria-current='{isActive(item) ? "location" : "false"}'
>
<span class="wrn-scrollspy__label">{item.label}</span>
</a>
</li>
{/each}
</ul>
<slot />
</nav>
}
style {
.wrn-scrollspy {
--scrollspy-accent: var(--wrn-color-primary);
max-width: 100%;
}
.wrn-scrollspy[data-color="secondary"] {
--scrollspy-accent: var(--wrn-color-secondary);
}
.wrn-scrollspy[data-color="success"] {
--scrollspy-accent: var(--wrn-color-success);
}
.wrn-scrollspy[data-color="danger"] {
--scrollspy-accent: var(--wrn-color-danger);
}
.wrn-scrollspy[data-color="info"] {
--scrollspy-accent: var(--wrn-color-info);
}
.wrn-scrollspy[data-size="sm"] {
font-size: 0.82rem;
}
.wrn-scrollspy[data-size="lg"] {
font-size: 1rem;
}
.wrn-scrollspy__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-scrollspy__list {
display: grid;
gap: 0.1rem;
margin: 0;
padding: 0;
list-style: none;
border-left: 1px solid var(--wrn-color-border);
}
.wrn-scrollspy__link {
display: block;
padding: 0.3rem 0.75rem;
margin-left: -1px;
border-left: 2px solid transparent;
color: var(--wrn-color-text-muted);
font-size: 0.85rem;
text-decoration: none;
}
.wrn-scrollspy__link:hover {
color: var(--wrn-color-text);
}
.wrn-scrollspy__link:focus-visible {
outline: 2px solid var(--scrollspy-accent);
outline-offset: -2px;
}
.wrn-scrollspy__link[data-active="true"] {
border-left-color: var(--scrollspy-accent);
color: var(--scrollspy-accent);
font-weight: 600;
}
/*
* A table of contents is a sidebar affordance. On a phone it stops being
* a rail and becomes a horizontal strip that scrolls, so it costs one
* line rather than a screenful.
*/
@media (max-width: 767px) {
.wrn-scrollspy__list {
grid-auto-flow: column;
grid-auto-columns: max-content;
overflow-x: auto;
border-left: 0;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-scrollspy__link {
margin-left: 0;
border-left: 0;
border-bottom: 2px solid transparent;
white-space: nowrap;
}
.wrn-scrollspy__link[data-active="true"] {
border-left-color: transparent;
border-bottom-color: var(--scrollspy-accent);
}
}
}
}
-150
View File
@@ -1,150 +0,0 @@
component SearchBox {
outputs {
search(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
clear(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Search Box"
name: string = ""
value: string = ""
placeholder: string = ""
type: string = "search"
min: string = ""
max: string = ""
step: string = ""
disabled: boolean = false
required: boolean = false
class: string = ""
}
state query = value
view {
<form
data-ui-component="SearchBox"
role="search"
class='wrn-search-box {class}'
@submit='event.preventDefault(); output.search({ value: query, name: name })'
>
<label
class="wrn-search-box__label"
data-hidden='{label === "" ? "true" : "false"}'
>
{label}
</label>
<div class="wrn-search-box__control">
<span
class="icon-[lucide--search] wrn-search-box__search-icon"
aria-hidden="true"
></span>
<input
name='{name}'
type='{type}'
value='{query}'
placeholder='{placeholder}'
min='{min}'
max='{max}'
step='{step}'
disabled='{disabled}'
required='{required}'
autocomplete="off"
class="wrn-search-box__input"
data-size='{size}'
@input='query = event.target.value'
@change='query = event.target.value'
/>
<button
type="button"
aria-label="Clear search"
data-show='query.length > 0 && !disabled'
class="wrn-search-box__button wrn-search-box__clear"
@click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); output.clear({ value: "", name: name })'
>
<span class="icon-[lucide--x] wrn-search-box__button-icon" aria-hidden="true"></span>
</button>
<button
type="submit"
aria-label='{label || "Search"}'
disabled='{disabled}'
class="wrn-search-box__button wrn-search-box__submit"
>
<span class="icon-[lucide--arrow-right] wrn-search-box__button-icon" aria-hidden="true"></span>
</button>
</div>
</form>
}
style {
.wrn-search-box { width: 100%; }
.wrn-search-box__label {
display: block;
margin-bottom: 0.5rem;
color: var(--wrn-color-text);
font-size: 0.875rem;
font-weight: 600;
}
.wrn-search-box__label[data-hidden="true"] {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.wrn-search-box__control { position: relative; display: flex; align-items: center; }
.wrn-search-box__search-icon {
position: absolute;
left: 1rem;
width: 1.25rem;
height: 1.25rem;
color: var(--wrn-color-input-placeholder);
pointer-events: none;
}
.wrn-search-box__input {
width: 100%;
height: 3rem;
padding: 0 6rem 0 3rem;
border: 1px solid var(--wrn-color-input-border);
border-radius: var(--wrn-radius);
outline: none;
color: var(--wrn-color-input-text);
background: var(--wrn-color-input-background);
transition: border-color var(--wrn-motion-base) var(--wrn-ease-standard), box-shadow var(--wrn-motion-base) var(--wrn-ease-standard);
}
.wrn-search-box__input[data-size="sm"] { height: 2.5rem; }
.wrn-search-box__input[data-size="lg"] { height: 3.5rem; }
.wrn-search-box__input::placeholder { color: var(--wrn-color-input-placeholder); }
.wrn-search-box__input:hover { border-color: var(--wrn-color-input-border-hover); }
.wrn-search-box__input:focus {
border-color: var(--wrn-color-input-border-focus);
box-shadow: 0 0 0 2px var(--wrn-color-focus);
}
.wrn-search-box__input:disabled, .wrn-search-box__button:disabled { cursor: not-allowed; opacity: 0.6; }
.wrn-search-box__button {
position: absolute;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
border-radius: var(--wrn-radius-sm);
transition: color var(--wrn-motion-base), background var(--wrn-motion-base);
}
.wrn-search-box__clear { right: 3rem; width: 2rem; height: 2rem; color: var(--wrn-color-text-muted); background: transparent; }
.wrn-search-box__clear:hover { color: var(--wrn-color-text); background: var(--wrn-color-surface-soft); }
.wrn-search-box__submit { right: 0.5rem; width: 2.25rem; height: 2.25rem; color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); }
.wrn-search-box__submit:hover { background: var(--wrn-color-primary-hover); }
.wrn-search-box__button:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: 2px; }
.wrn-search-box__button-icon { width: 1rem; height: 1rem; }
}
}
-335
View File
@@ -1,335 +0,0 @@
// Sidebar -- a vertical navigation rail that becomes a drawer on small screens.
//
// <Sidebar items='[{"label":"Dashboard","href":"/","value":"dash"}]' active="dash" />
//
// An entry is one of three shapes:
// { label, href, value } a single link
// { heading, items: [...] } a labelled group
// { label, items: [...] } a collapsible branch, nested to 3 levels
//
// The off-canvas presentation is Drawer rather than a second implementation,
// so the focus trap and the body scroll lock come from one place.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
import Drawer from "./Drawer.wrn"
component Sidebar {
outputs {
toggle(payload: { open: boolean })
open(payload: { sourceEvent: Event })
close(payload: { source: string })
select(payload: { item: object; value: string; level: number })
}
props {
color: string = "primary"
size: string = "default"
label: string = "Sidebar"
items: unknown[] = []
active: string = ""
mobileLabel: string = "Open navigation"
drawerTitle: string = "Navigation"
class: string = ""
}
state drawerOpen = false
functions {
shared function entryList() {
return Array.isArray(items) ? items : []
}
// Accepts children as well as items: children is what shipped in 0.8.5
// and existing callers should keep working.
shared function childrenOf(entry) {
if (!entry) {
return []
}
if (Array.isArray(entry.items)) {
return entry.items
}
return Array.isArray(entry.children) ? entry.children : []
}
shared function isGroup(entry) {
return Boolean(entry.heading)
}
shared function isActive(entry) {
return Boolean(entry.value) && entry.value === active
}
client function choose(entry, level) {
if (entry.disabled) {
return
}
output.select({ item: entry, value: entry.value || "", level: level })
if (drawerOpen) {
drawerOpen = false
output.close({ source: "select" })
}
}
client function openDrawer(sourceEvent) {
drawerOpen = true
output.open({ sourceEvent: sourceEvent })
output.toggle({ open: true })
}
client function closeDrawer() {
drawerOpen = false
output.close({ source: "drawer" })
output.toggle({ open: false })
}
}
view {
<div
{...attrs}
data-ui-component="Sidebar"
class='wrn-sidebar {class}'
data-color='{color}'
data-size='{size}'
>
<button
type="button"
class="wrn-sidebar__launcher"
aria-label='{mobileLabel}'
aria-expanded='{drawerOpen}'
@click='openDrawer(event)'
>
<span class="wrn-sidebar__launcher-bar" aria-hidden="true"></span>
<span class="wrn-sidebar__launcher-text">{mobileLabel}</span>
</button>
<nav class="wrn-sidebar__rail" aria-label='{label}'>
<ul class="wrn-sidebar__list" data-wrn-roving="vertical">
{#each entryList() as entry}
<li class="wrn-sidebar__entry" data-group='{isGroup(entry)}'>
<p class="wrn-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
<a
class="wrn-sidebar__link"
data-show="!entry.heading"
href='{entry.href || "#"}'
data-wrn-roving-item="true"
data-active='{isActive(entry)}'
aria-current='{isActive(entry) ? "page" : "false"}'
aria-disabled='{entry.disabled ? "true" : "false"}'
@click='choose(entry, 1)'
>
<span
class='wrn-sidebar__icon {entry.icon}'
data-show="entry.icon"
aria-hidden="true"
></span>
<span class="wrn-sidebar__label">{entry.label}</span>
<span class="wrn-sidebar__badge" data-show="entry.badge">{entry.badge}</span>
</a>
<ul class="wrn-sidebar__sublist" data-show="childrenOf(entry).length > 0">
{#each childrenOf(entry) as child}
<li class="wrn-sidebar__entry">
<a
class="wrn-sidebar__link"
href='{child.href || "#"}'
data-active='{isActive(child)}'
aria-current='{isActive(child) ? "page" : "false"}'
aria-disabled='{child.disabled ? "true" : "false"}'
@click='choose(child, 2)'
>
<span
class='wrn-sidebar__icon {child.icon}'
data-show="child.icon"
aria-hidden="true"
></span>
<span class="wrn-sidebar__label">{child.label}</span>
<span class="wrn-sidebar__badge" data-show="child.badge">{child.badge}</span>
</a>
<ul
class="wrn-sidebar__sublist wrn-sidebar__sublist--level3"
data-show="childrenOf(child).length > 0"
>
{#each childrenOf(child) as leaf}
<li class="wrn-sidebar__entry">
<a
class="wrn-sidebar__link"
href='{leaf.href || "#"}'
data-active='{isActive(leaf)}'
aria-current='{isActive(leaf) ? "page" : "false"}'
@click='choose(leaf, 3)'
>
<span class="wrn-sidebar__label">{leaf.label}</span>
</a>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
<slot />
</nav>
<Drawer
open={drawerOpen}
placement="left"
title={drawerTitle}
label={label}
@close="closeDrawer()"
>
<slot name="drawer"></slot>
</Drawer>
</div>
}
style {
.wrn-sidebar {
--sidebar-accent: var(--wrn-color-primary);
max-width: 100%;
}
.wrn-sidebar[data-color="secondary"] {
--sidebar-accent: var(--wrn-color-secondary);
}
.wrn-sidebar[data-color="success"] {
--sidebar-accent: var(--wrn-color-success);
}
.wrn-sidebar[data-color="danger"] {
--sidebar-accent: var(--wrn-color-danger);
}
.wrn-sidebar[data-color="info"] {
--sidebar-accent: var(--wrn-color-info);
}
.wrn-sidebar[data-size="sm"] {
font-size: 0.82rem;
}
.wrn-sidebar[data-size="lg"] {
font-size: 1rem;
}
.wrn-sidebar__rail {
width: 100%;
}
.wrn-sidebar__list,
.wrn-sidebar__sublist {
display: grid;
gap: 0.1rem;
margin: 0;
padding: 0;
list-style: none;
}
.wrn-sidebar__sublist {
margin-left: 0.85rem;
padding-left: 0.5rem;
border-left: 1px solid var(--wrn-color-border);
}
.wrn-sidebar__entry[data-group="true"] + .wrn-sidebar__entry {
margin-top: 0.35rem;
}
.wrn-sidebar__heading {
margin: 0.9rem 0 0.35rem;
color: var(--wrn-color-text-muted);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.wrn-sidebar__link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.6rem;
border-radius: var(--wrn-radius-sm);
color: var(--wrn-color-text-muted);
font-size: 0.88rem;
font-weight: 600;
text-decoration: none;
}
.wrn-sidebar__link:hover {
background: var(--wrn-color-surface-soft);
color: var(--wrn-color-text);
}
.wrn-sidebar__link:focus-visible {
outline: 2px solid var(--sidebar-accent);
outline-offset: 2px;
}
.wrn-sidebar__link[data-active="true"] {
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
color: var(--sidebar-accent);
}
.wrn-sidebar__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wrn-sidebar__label {
min-width: 0;
}
.wrn-sidebar__badge {
margin-left: auto;
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
color: var(--sidebar-accent);
font-size: 0.72rem;
}
.wrn-sidebar__launcher {
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-sidebar__launcher-bar {
width: 1rem;
height: 2px;
background: currentColor;
box-shadow:
0 -5px 0 currentColor,
0 5px 0 currentColor;
}
/*
* Below the tablet breakpoint the rail is replaced by the drawer launcher.
* A permanent rail eats most of a phone screen, and Drawer already handles
* the focus trap and the scroll lock.
*/
@media (max-width: 767px) {
.wrn-sidebar__launcher {
display: inline-flex;
}
.wrn-sidebar__rail {
display: none;
}
}
}
}
-442
View File
@@ -1,442 +0,0 @@
// Stepper -- ordered progress through a sequence.
//
// <Stepper steps='[{"label":"Account"},{"label":"Billing"}]' active={1} />
//
// Each step can be authored by hand instead of using the built-in body, by
// passing a slot named for its index:
//
// <Stepper steps={steps} active={1}>
// <div data-slot="step-1">...anything...</div>
// </Stepper>
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Stepper {
outputs {
change(payload: { index: number; step: object })
back(payload: { index: number; step: object })
next(payload: { index: number; step: object })
skip(payload: { index: number; step: object })
finish(payload: { index: number; step: object })
}
props {
color: string = "primary"
size: string = "default"
steps: unknown[] = []
active: number = 0
orientation: string = "horizontal"
clickable: boolean = false
label: string = "Progress"
// Render each step body, showing only the active one -- same contract as
// Tabs, so a wizard does not have to hand-roll the panel switching.
showPanel: boolean = false
controls: boolean = false
allowSkip: boolean = false
// Lets a form hold the step. The component never validates anything
// itself: the page owns the form, so it sets this while the step is
// incomplete and clears it once the step passes.
nextDisabled: boolean = false
backLabel: string = "Back"
nextLabel: string = "Next"
skipLabel: string = "Skip"
finishLabel: string = "Finish"
class: string = ""
}
functions {
shared function stepList() {
return Array.isArray(steps) ? steps : []
}
shared function activeIndex() {
var count = stepList().length
if (count < 1) {
return 0
}
var value = Number(active)
if (!value || value < 0) {
return 0
}
return Math.min(value, count - 1)
}
shared function statusFor(index) {
if (index < activeIndex()) {
return "complete"
}
if (index === activeIndex()) {
return "current"
}
return "upcoming"
}
// An empty orientation is how the roving runtime is told to stay out of
// the way, so a read-only stepper never takes arrow-key focus.
shared function rovingAxis() {
if (!clickable) {
return ""
}
return orientation === "vertical" ? "vertical" : "horizontal"
}
shared function isActiveStep(index) {
return index === activeIndex()
}
shared function lastIndex() {
return Math.max(0, stepList().length - 1)
}
shared function onLastStep() {
return activeIndex() >= lastIndex()
}
shared function activeStep() {
var list = stepList()
return list.length ? list[activeIndex()] : {}
}
client function goBack() {
var target = Math.max(0, activeIndex() - 1)
output.back({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function goNext() {
if (nextDisabled) {
return
}
if (onLastStep()) {
output.finish({ index: activeIndex(), step: activeStep() })
return
}
var target = Math.min(lastIndex(), activeIndex() + 1)
output.next({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function goSkip() {
var target = Math.min(lastIndex(), activeIndex() + 1)
output.skip({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function selectStep(index, step) {
if (!clickable) {
return
}
output.change({ index: index, step: step })
}
}
view {
<div
{...attrs}
data-ui-component="Stepper"
class='wrn-stepper {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-clickable='{clickable}'
>
<ol
class="wrn-stepper__list"
data-wrn-roving='{rovingAxis()}'
aria-label='{label}'
>
{#each stepList() as step, index}
<li
class="wrn-stepper__step"
data-status='{statusFor(index)}'
aria-current='{statusFor(index) === "current" ? "step" : "false"}'
>
<button
type="button"
class="wrn-stepper__button"
data-wrn-roving-item='{clickable ? "true" : "false"}'
@click='selectStep(index, step)'
>
<span class="wrn-stepper__marker" aria-hidden="true">
<span class='wrn-stepper__icon {step.icon}' data-show="step.icon"></span>
<span class="wrn-stepper__number" data-show="!step.icon">{index + 1}</span>
</span>
<span class="wrn-stepper__body">
<span class="wrn-stepper__label">{step.label}</span>
<span class="wrn-stepper__description" data-show="step.description">
{step.description}
</span>
</span>
</button>
<span class="wrn-stepper__custom">
<slot name="step-{index}"></slot>
</span>
</li>
{/each}
</ol>
<div class="wrn-stepper__panels" data-show="showPanel">
{#each stepList() as step, index}
<div
class="wrn-stepper__panel"
data-show='isActiveStep(index)'
aria-hidden='{index === activeIndex() ? "false" : "true"}'
>
<h4 class="wrn-stepper__panel-title" data-show="step.title">{step.title}</h4>
<p class="wrn-stepper__panel-body" data-show="step.content">{step.content}</p>
<slot name="panel-{index}"></slot>
</div>
{/each}
</div>
<div class="wrn-stepper__controls" data-show="controls">
<button
type="button"
class="wrn-stepper__button-control wrn-stepper__back"
disabled='{activeIndex() === 0}'
@click='goBack()'
>
{backLabel}
</button>
<span class="wrn-stepper__controls-spacer"></span>
<button
type="button"
class="wrn-stepper__button-control wrn-stepper__skip"
data-show="allowSkip && !onLastStep()"
@click='goSkip()'
>
{skipLabel}
</button>
<button
type="button"
class="wrn-stepper__button-control wrn-stepper__next"
data-primary="true"
disabled='{nextDisabled}'
@click='goNext()'
>
{onLastStep() ? finishLabel : nextLabel}
</button>
</div>
<slot />
</div>
}
style {
.wrn-stepper {
--stepper-accent: var(--wrn-color-primary);
max-width: 100%;
}
.wrn-stepper__list {
display: flex;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
max-width: 100%;
}
.wrn-stepper[data-color="secondary"] {
--stepper-accent: var(--wrn-color-secondary);
}
.wrn-stepper[data-color="success"] {
--stepper-accent: var(--wrn-color-success);
}
.wrn-stepper[data-color="danger"] {
--stepper-accent: var(--wrn-color-danger);
}
.wrn-stepper[data-color="info"] {
--stepper-accent: var(--wrn-color-info);
}
.wrn-stepper[data-size="sm"] {
font-size: 0.82rem;
}
.wrn-stepper[data-size="lg"] {
font-size: 1rem;
}
.wrn-stepper[data-orientation="vertical"] .wrn-stepper__list {
flex-direction: column;
}
.wrn-stepper__step {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-width: 0;
gap: 0.35rem;
}
.wrn-stepper__button {
appearance: none;
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.5rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: default;
}
.wrn-stepper[data-clickable="true"] .wrn-stepper__button {
cursor: pointer;
}
.wrn-stepper[data-clickable="true"] .wrn-stepper__button:hover {
background: var(--wrn-color-surface-soft);
}
.wrn-stepper__button:focus-visible {
outline: 2px solid var(--stepper-accent);
outline-offset: 2px;
}
.wrn-stepper__marker {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 2rem;
height: 2rem;
border: 1px solid var(--wrn-color-border);
border-radius: 999px;
background: var(--wrn-color-surface);
font-size: 0.85rem;
font-weight: 700;
}
.wrn-stepper__step[data-status="complete"] .wrn-stepper__marker {
border-color: var(--stepper-accent);
background: var(--stepper-accent);
color: var(--wrn-color-primary-contrast);
}
.wrn-stepper__step[data-status="current"] .wrn-stepper__marker {
border-color: var(--stepper-accent);
color: var(--stepper-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--stepper-accent) 22%, transparent);
}
.wrn-stepper__step[data-status="upcoming"] .wrn-stepper__marker {
color: var(--wrn-color-text-muted);
}
.wrn-stepper__body {
display: flex;
flex-direction: column;
min-width: 0;
}
.wrn-stepper__label {
font-size: 0.9rem;
font-weight: 600;
}
.wrn-stepper__description {
color: var(--wrn-color-text-muted);
font-size: 0.78rem;
}
.wrn-stepper__step[data-status="upcoming"] .wrn-stepper__label {
color: var(--wrn-color-text-muted);
}
.wrn-stepper__panels {
margin-top: 1rem;
}
.wrn-stepper__panel {
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
animation: wrn-stepper-in 200ms ease both;
}
.wrn-stepper__panel-title {
margin: 0 0 0.35rem;
font-size: 0.95rem;
font-weight: 700;
}
.wrn-stepper__panel-body {
margin: 0;
color: var(--wrn-color-text-muted);
line-height: 1.6;
}
.wrn-stepper__controls {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.85rem;
}
.wrn-stepper__controls-spacer {
flex: 1 1 auto;
}
.wrn-stepper__button-control {
appearance: none;
padding: 0.45rem 0.9rem;
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.86rem;
font-weight: 600;
cursor: pointer;
}
.wrn-stepper__button-control:hover:not(:disabled) {
background: var(--wrn-color-surface-soft);
}
.wrn-stepper__button-control:focus-visible {
outline: 2px solid var(--stepper-accent);
outline-offset: 2px;
}
.wrn-stepper__button-control[data-primary="true"] {
border-color: var(--stepper-accent);
background: var(--stepper-accent);
color: var(--wrn-color-primary-contrast);
}
/* A held step has to look held, or the button reads as broken. */
.wrn-stepper__button-control:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@keyframes wrn-stepper-in {
from {
opacity: 0;
transform: translateX(0.75rem);
}
to {
opacity: 1;
transform: none;
}
}
/* A horizontal stepper cannot stay side by side on a phone. */
@media (max-width: 639px) {
.wrn-stepper__list {
flex-direction: column;
}
}
}
}
-979
View File
@@ -1,979 +0,0 @@
component StrongPassword {
outputs {
input(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
change(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
strength(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Password"
name: string = "password"
value: string = ""
placeholder: string = "Create a strong password"
autocomplete: string = "new-password"
minLength: number = 8
specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"
requireLowercase: boolean = true
requireUppercase: boolean = true
requireNumber: boolean = true
requireSpecialCharacter: boolean = true
showRequirements: boolean = true
presentation: string = "inline"
hintText: string = "Use a unique password you do not use elsewhere."
emptyLabel: string = "Enter a password"
weakLabel: string = "Weak"
fairLabel: string = "Fair"
goodLabel: string = "Good"
strongLabel: string = "Strong"
disabled: boolean = false
readonly: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
class: string = ""
}
state password = value
state detailsOpen: boolean = false
functions {
shared function hasMinimumLength() {
return password.length >= Number(minLength)
}
shared function hasLowercase() {
return password !== password.toUpperCase()
}
shared function hasUppercase() {
return password !== password.toLowerCase()
}
shared function hasNumber() {
return containsCharacterAt("0123456789", 0)
}
shared function hasSpecialCharacter() {
return containsCharacterAt(specialCharactersSet, 0)
}
shared function containsCharacterAt(characters, index) {
if (index >= characters.length) {
return false
}
if (password.includes(characters[index])) {
return true
}
return containsCharacterAt(characters, index + 1)
}
shared function score() {
return (hasMinimumLength() ? 1 : 0) + (requireLowercase && hasLowercase() ? 1 : 0) + (requireUppercase && hasUppercase() ? 1 : 0) + (requireNumber && hasNumber() ? 1 : 0) + (requireSpecialCharacter && hasSpecialCharacter() ? 1 : 0)
}
shared function maximumScore() {
return 1 + (requireLowercase ? 1 : 0) + (requireUppercase ? 1 : 0) + (requireNumber ? 1 : 0) + (requireSpecialCharacter ? 1 : 0)
}
shared function strengthPercent() {
return Math.round(score() / maximumScore() * 100)
}
shared function strengthLevel() {
if (!password) {
return "empty"
}
if (strengthPercent() <= 25) {
return "weak"
}
if (strengthPercent() <= 50) {
return "fair"
}
if (strengthPercent() < 100) {
return "good"
}
return "strong"
}
shared function strengthLabel() {
if (strengthLevel() === "weak") {
return weakLabel
}
if (strengthLevel() === "fair") {
return fairLabel
}
if (strengthLevel() === "good") {
return goodLabel
}
if (strengthLevel() === "strong") {
return strongLabel
}
return emptyLabel
}
}
view {
<div
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--strong-password wrn-next--strong-password-{presentation} {invalid ? 'wrn-next--invalid' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
data-wrn-strong-password
data-strength="{password ? strengthLevel() : 'empty'}"
data-score="{password ? score() : 0}"
>
<label for="{name}-strong-password">{label}</label>
<div class="wrn-next__strong-password-anchor">
<input
{...attrs}
id="{name}-strong-password"
type="password"
name="{name}"
value="{password}"
placeholder="{placeholder}"
autocomplete="{autocomplete}"
minlength="{minLength}"
disabled="{disabled}"
readonly="{readonly}"
required="{required}"
aria-invalid="{invalid ? 'true' : 'false'}"
aria-describedby="{validationMessage ? name + '-validation' : hintText ? name + '-hint' : ''}"
@focus="detailsOpen = true"
@blur="detailsOpen = false"
@input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
/>
{#if presentation === "popover"}
<div
class="wrn-next__strong-password-popover"
data-open="{detailsOpen ? 'true' : 'false'}"
role="status"
>
<div class="wrn-next__strong-password-popover-arrow" aria-hidden="true"></div>
<div class="wrn-next__strong-password-details">
<div class="wrn-next__strong-password-summary">
<span>Password strength</span>
<strong>{password ? strengthLabel() : emptyLabel}</strong>
</div>
<div
class="wrn-next__strong-password-meter"
role="progressbar"
aria-label="Password strength"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{password ? strengthPercent() : 0}"
>
<span style="width: {password ? strengthPercent() : 0}%"></span>
</div>
{#if showRequirements}
<ul class="wrn-next__strong-password-requirements">
<li data-met="{password && hasMinimumLength() ? 'true' : 'false'}">
<span aria-hidden="true"></span>At least {minLength} characters
</li>
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
{#if requireSpecialCharacter}<li class="wrn-next__strong-password-special" data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
</ul>
{/if}
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
</div>
</div>
{/if}
</div>
{#if presentation !== "popover"}
<div class="wrn-next__strong-password-details">
<div class="wrn-next__strong-password-summary">
<span>Password strength</span>
<strong>{password ? strengthLabel() : emptyLabel}</strong>
</div>
<div
class="wrn-next__strong-password-meter"
role="progressbar"
aria-label="Password strength"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{password ? strengthPercent() : 0}"
>
<span style="width: {password ? strengthPercent() : 0}%"></span>
</div>
{#if showRequirements}
<ul class="wrn-next__strong-password-requirements">
<li data-met="{password && hasMinimumLength() ? 'true' : 'false'}"><span aria-hidden="true"></span>At least {minLength} characters</li>
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
{#if requireSpecialCharacter}<li class="wrn-next__strong-password-special" data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
</ul>
{/if}
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
</div>
{/if}
<small id="{name}-validation" class="wrn-next__validation" data-error="{name}">{validationMessage}</small>
</div>
}
style {
.wrn-next--strong-password {
position: relative;
display: grid;
width: 100%;
gap: 0.6rem;
color: var(--wrn-color-text);
}
.wrn-next--strong-password > label {
font-size: 0.82em;
font-weight: 700;
}
.wrn-next__strong-password-anchor {
position: relative;
}
.wrn-next--strong-password input {
width: 100%;
min-height: 2.65rem;
padding: 0.7rem 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
outline: 0;
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
font: inherit;
transition:
border-color 160ms ease,
box-shadow 160ms ease;
}
.wrn-next--strong-password input:focus-visible {
border-color: var(--wrn-component-color);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 16%, transparent);
}
.wrn-next__strong-password-details {
display: grid;
gap: 0.65rem;
}
.wrn-next__strong-password-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
color: var(--wrn-color-muted);
font-size: 0.75em;
}
.wrn-next__strong-password-summary strong {
color: var(--wrn-color-text);
}
.wrn-next__strong-password-meter {
width: 100%;
height: 0.42rem;
overflow: hidden;
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-color-border) 72%, transparent);
}
.wrn-next__strong-password-meter > span {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: var(--wrn-color-danger);
transition:
width 180ms ease,
background-color 180ms ease;
}
.wrn-next--strong-password[data-strength="fair"] .wrn-next__strong-password-meter > span {
background: var(--wrn-color-warning);
}
.wrn-next--strong-password[data-strength="good"] .wrn-next__strong-password-meter > span {
background: var(--wrn-component-color);
}
.wrn-next--strong-password[data-strength="strong"] .wrn-next__strong-password-meter > span {
background: var(--wrn-color-success);
}
.wrn-next__strong-password-requirements {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.45rem 1rem;
margin: 0;
padding: 0;
list-style: none;
}
.wrn-next__strong-password-requirements li {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 0.45rem;
color: var(--wrn-color-muted);
font-size: 0.72em;
line-height: 1.45;
}
.wrn-next__strong-password-requirements .wrn-next__strong-password-special {
grid-column: 1 / -1;
white-space: nowrap;
}
.wrn-next__strong-password-special [data-text="specialCharactersSet"] {
white-space: nowrap;
}
.wrn-next__strong-password-requirements li > span[aria-hidden="true"] {
position: relative;
width: 0.9rem;
height: 0.9rem;
flex: 0 0 auto;
margin-top: 0.08rem;
border: 1px solid currentColor;
border-radius: 50%;
}
.wrn-next__strong-password-requirements li > span[aria-hidden="true"]::after {
position: absolute;
top: 0.12rem;
left: 0.29rem;
width: 0.22rem;
height: 0.42rem;
border: solid var(--wrn-color-surface);
border-width: 0 0.1rem 0.1rem 0;
content: "";
opacity: 0;
transform: rotate(45deg);
}
.wrn-next__strong-password-requirements li[data-met="true"] {
color: var(--wrn-color-success);
}
.wrn-next__strong-password-requirements li[data-met="true"] > span[aria-hidden="true"] {
border-color: var(--wrn-color-success);
background: var(--wrn-color-success);
}
.wrn-next__strong-password-requirements li[data-met="true"] > span[aria-hidden="true"]::after {
opacity: 1;
}
.wrn-next__strong-password-details > small {
color: var(--wrn-color-muted);
font-size: 0.7em;
line-height: 1.5;
}
.wrn-next__strong-password-popover {
position: absolute;
top: calc(100% + 0.7rem);
left: 0;
z-index: 30;
width: min(28rem, calc(100vw - 2rem));
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius);
opacity: 0;
visibility: hidden;
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-2);
transform: translateY(-0.35rem);
transition:
opacity 140ms ease,
transform 140ms ease,
visibility 140ms ease;
}
.wrn-next__strong-password-popover[data-open="true"] {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.wrn-next__strong-password-popover-arrow {
position: absolute;
top: -0.38rem;
left: 1.25rem;
width: 0.7rem;
height: 0.7rem;
border-top: 1px solid var(--wrn-color-border);
border-left: 1px solid var(--wrn-color-border);
background: var(--wrn-color-surface);
transform: rotate(45deg);
}
@media (max-width: 560px) {
.wrn-next__strong-password-requirements {
grid-template-columns: 1fr;
}
}
/* 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);
}
}
}
-103
View File
@@ -1,103 +0,0 @@
component StyledIcon {
props {
size: string = "default"
color: string = "primary"
title: string = "Styled Icon"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--styled-icon wrn-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
style {
.wrn-next--styled-icon {
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--styled-icon > p { margin: 0; color: var(--wrn-color-muted); }
.wrn-next--styled-icon > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-next--styled-icon > .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;
}
}
}
-362
View File
@@ -1,362 +0,0 @@
// Tabs -- a tablist over panels.
//
// <Tabs items='[{"label":"Overview","value":"overview"}]' active="overview" />
//
// With mode="url" the selection is mirrored into a query parameter using
// history.pushState, so the panel swaps without a page load and the back
// button works. The query parameter is used rather than the hash because it
// survives a reload and does not collide with in-page anchors or Scrollspy.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Tabs {
outputs {
change(payload: { value: string; item: object; index: number })
select(payload: { value: string; item: object; index: number })
}
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 = ""
}
state activeValue = ""
// Which way the panel should slide in. Set on every change so the animation
// follows the direction of travel rather than always coming from one side.
state slideFrom = "forward"
functions {
shared function itemList() {
return Array.isArray(items) ? items : []
}
shared function valueOf(item, index) {
return String(item.value || item.id || index)
}
/*
* In url mode the query parameter is the source of truth, not local state.
*
* The client router owns popstate and swaps the whole page on back and
* forward, which discards component state anyway. Reading the URL means
* the right tab simply falls out of whatever render happens next, with no
* listener to lose and nothing to keep in step.
*/
shared function currentValue() {
if (mode === "url" && typeof window !== "undefined" && window.location) {
var fromUrl = new URLSearchParams(window.location.search).get(param || "tab")
return fromUrl ? fromUrl : defaultValue()
}
if (activeValue) {
return activeValue
}
return defaultValue()
}
// The selection this instance started with. The runtime falls back to it
// when the back button lands on a URL that has no tab parameter at all.
shared function defaultValue() {
if (active) {
return active
}
var list = itemList()
return list.length ? valueOf(list[0], 0) : ""
}
shared function isSelected(item, index) {
return valueOf(item, index) === currentValue()
}
shared function rovingAxis() {
return orientation === "vertical" ? "vertical" : "horizontal"
}
client function selectTab(item, index, sourceEvent) {
if (item.disabled) {
return
}
var value = valueOf(item, index)
var list = itemList()
var previous = -1
for (var scan = 0; scan < list.length; scan += 1) {
if (valueOf(list[scan], scan) === currentValue()) {
previous = scan
}
}
slideFrom = previous > index ? "back" : "forward"
activeValue = value
if (mode === "url" && window.history && window.history.pushState) {
var url = new URL(window.location.href)
url.searchParams.set(param || "tab", value)
window.history.pushState({}, "", url.toString())
}
output.change({ value: value, item: item, index: index })
output.select({ value: value, item: item, index: index })
}
}
view {
<section
{...attrs}
data-ui-component="Tabs"
class='wrn-tabs {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-slide='{slideFrom}'
data-mode='{mode}'
data-param='{param}'
>
<div
class="wrn-tabs__list"
role="tablist"
aria-label='{label}'
aria-orientation='{orientation}'
data-wrn-roving='{rovingAxis()}'
>
{#each itemList() as item, index}
<button
type="button"
class="wrn-tabs__tab"
role="tab"
data-value='{valueOf(item, index)}'
data-wrn-roving-item="true"
id='wrn-tab-{valueOf(item, index)}'
aria-controls='wrn-panel-{valueOf(item, index)}'
aria-selected='{isSelected(item, index) ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
@click='selectTab(item, index, event)'
>
<span class='wrn-tabs__icon {item.icon}' data-show="item.icon" aria-hidden="true"></span>
<span class="wrn-tabs__label">{item.label || item.title}</span>
<span class="wrn-tabs__badge" data-show="item.badge">{item.badge}</span>
</button>
{/each}
</div>
<div class="wrn-tabs__panels">
{#each itemList() as item, index}
<div
class="wrn-tabs__panel"
role="tabpanel"
id='wrn-panel-{valueOf(item, index)}'
aria-labelledby='wrn-tab-{valueOf(item, index)}'
tabindex="0"
data-show='isSelected(item, index)'
>
<h3 class="wrn-tabs__title" data-show="item.title && item.title !== item.label">
{item.title}
</h3>
<p class="wrn-tabs__description" data-show="item.description">{item.description}</p>
<div class="wrn-tabs__content" data-show="item.content">{item.content}</div>
<slot name="panel-{valueOf(item, index)}"></slot>
</div>
{/each}
<slot />
</div>
</section>
}
style {
.wrn-tabs {
--tabs-accent: var(--wrn-color-primary);
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 100%;
}
.wrn-tabs[data-color="secondary"] {
--tabs-accent: var(--wrn-color-secondary);
}
.wrn-tabs[data-color="success"] {
--tabs-accent: var(--wrn-color-success);
}
.wrn-tabs[data-color="danger"] {
--tabs-accent: var(--wrn-color-danger);
}
.wrn-tabs[data-color="info"] {
--tabs-accent: var(--wrn-color-info);
}
.wrn-tabs[data-size="sm"] {
font-size: 0.82rem;
}
.wrn-tabs[data-size="lg"] {
font-size: 1rem;
}
.wrn-tabs[data-orientation="vertical"] {
flex-direction: row;
align-items: flex-start;
}
.wrn-tabs__list {
display: flex;
gap: 0.25rem;
min-width: 0;
padding: 0.25rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface-soft);
/* A long tablist scrolls rather than wrapping into an unusable stack. */
overflow-x: auto;
}
.wrn-tabs[data-orientation="vertical"] .wrn-tabs__list {
flex-direction: column;
flex: 0 0 auto;
width: 14rem;
overflow-x: visible;
}
.wrn-tabs__tab {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
flex: 1 0 auto;
padding: 0.55rem 0.9rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: var(--wrn-color-text-muted);
font: inherit;
font-size: 0.88rem;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition:
background 160ms ease,
color 160ms ease;
}
.wrn-tabs[data-orientation="vertical"] .wrn-tabs__tab {
justify-content: flex-start;
}
.wrn-tabs__tab:hover {
color: var(--wrn-color-text);
}
.wrn-tabs__tab:focus-visible {
outline: 2px solid var(--tabs-accent);
outline-offset: 2px;
}
.wrn-tabs__tab[aria-selected="true"] {
background: var(--wrn-color-surface);
color: var(--tabs-accent);
box-shadow: var(--wrn-shadow-1);
}
.wrn-tabs__tab[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wrn-tabs__badge {
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--tabs-accent) 16%, transparent);
color: var(--tabs-accent);
font-size: 0.72rem;
}
.wrn-tabs__panels {
min-width: 0;
flex: 1 1 auto;
}
.wrn-tabs__panel {
outline: none;
animation: wrn-tabs-slide-forward 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.wrn-tabs[data-slide="back"] .wrn-tabs__panel {
animation-name: wrn-tabs-slide-back;
}
/* The panels clip their own slide so it never widens the page. */
.wrn-tabs__panels {
overflow-x: clip;
}
.wrn-tabs__panel:focus-visible {
outline: 2px solid var(--tabs-accent);
outline-offset: 4px;
border-radius: var(--wrn-radius-sm);
}
.wrn-tabs__title {
margin: 0 0 0.35rem;
font-size: 1rem;
font-weight: 700;
}
.wrn-tabs__description {
margin: 0;
color: var(--wrn-color-text-muted);
line-height: 1.6;
}
.wrn-tabs__content {
margin-top: 0.6rem;
}
@keyframes wrn-tabs-slide-forward {
from {
opacity: 0;
transform: translateX(1.25rem);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes wrn-tabs-slide-back {
from {
opacity: 0;
transform: translateX(-1.25rem);
}
to {
opacity: 1;
transform: none;
}
}
/* Respect a reduced-motion preference: swap instantly instead. */
@media (prefers-reduced-motion: reduce) {
.wrn-tabs__panel {
animation: none;
}
}
@media (max-width: 639px) {
.wrn-tabs[data-orientation="vertical"] {
flex-direction: column;
}
.wrn-tabs[data-orientation="vertical"] .wrn-tabs__list {
width: 100%;
flex-direction: row;
overflow-x: auto;
}
}
}
}
-651
View File
@@ -1,651 +0,0 @@
component TimePicker {
outputs {
input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
focus(payload: { value: string; name: string; sourceEvent: Event })
blur(payload: { value: string; name: string; sourceEvent: Event })
open(payload: { value: string; name: string; sourceEvent: Event })
close(payload: { value: string; name: string; sourceEvent: Event })
invalid(payload: { name: string; message: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Time"
hiddenLabel: boolean = false
value: string = ""
placeholder: string = ""
variant: string = "normal"
icon: string = "icon-[lucide--clock-3]"
iconPosition: string = "end"
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
min: string = ""
max: string = ""
step: string = ""
format: string = "24"
minuteStep: number = 5
hours = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]
minutes = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"]
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state currentValue = value
state selectedHour = value ? value.split(":")[0] : "00"
state selectedMinute = value ? value.split(":")[1] : "00"
state expanded: boolean = false
functions {
client function commit(part, nextValue, sourceEvent, next, detail) { if (part === "hour") { selectedHour = String(nextValue).padStart(2, "0") } if (part === "minute") { selectedMinute = String(nextValue).padStart(2, "0") } next = selectedHour + ":" + selectedMinute; if ((min && next < min) || (max && next > max)) { return } currentValue = next; detail = { value: currentValue, hour: selectedHour, minute: selectedMinute, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function selectTimePart(sourceEvent, root, input, parts, hourValue, minuteValue, next, trigger, detail) { root = sourceEvent.currentTarget.closest(".wrn-next--time-picker"); input = root.querySelector(".wrn-next__time-value"); parts = String(input.value || value || "00:00").split(":"); hourValue = sourceEvent.currentTarget.dataset.part === "hour" ? sourceEvent.currentTarget.dataset.value : parts[0]; minuteValue = sourceEvent.currentTarget.dataset.part === "minute" ? sourceEvent.currentTarget.dataset.value : parts[1]; next = hourValue + ":" + minuteValue; if ((min && next < min) || (max && next > max)) { return } currentValue = next; input.setAttribute("value", next); trigger = root.querySelector(".wrn-next__time-trigger span"); if (trigger) { trigger.replaceChildren(next) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: next, hour: hourValue, minute: minuteValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function openPicker(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function closePicker(sourceEvent) { expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--time-picker {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
<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__field-control" data-icon-position="{iconPosition}">
{#if icon}<span class="{icon} wrn-next__field-icon" aria-hidden="true"></span>{/if}
<input id="{id || name}" class="wrn-next__sr-only wrn-next__time-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" />
<button type="button" class="wrn-next__time-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openPicker(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select time"}</span><i class="{icon}" aria-hidden="true"></i></button>
</div>
<div class="wrn-next__time-panel" role="dialog" aria-label="{label}"><div><strong>Hour</strong><div class="wrn-next__time-options">{#each hours as hour}<button type="button" data-part="hour" data-value="{hour}" aria-pressed="{String(hour) === selectedHour}" @click="selectTimePart(event)">{hour}</button>{/each}</div></div><div><strong>Minute</strong><div class="wrn-next__time-options">{#each minutes as minute}<button type="button" data-part="minute" data-value="{minute}" aria-pressed="{String(minute) === selectedMinute}" @click="selectTimePart(event)">{minute}</button>{/each}</div></div><button type="button" class="wrn-next__time-done" @click="closePicker(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__time-panel {
grid-template-columns: 1fr 1fr;
}
.wrn-next__time-options {
display: grid;
max-height: 12rem;
margin-top: 0.4rem;
overflow-y: auto;
gap: 0.2rem;
}
.wrn-next__time-done {
align-self: end;
background: var(--wrn-field-color) !important;
color: var(--wrn-color-primary-contrast, #fff) !important;
}
@media (max-width: 640px) {
.wrn-next__time-panel {
grid-template-columns: 1fr;
}
}
/* 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);
}
}
}
-114
View File
@@ -1,114 +0,0 @@
component Timeline {
outputs {
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Timeline"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section
data-ui-component="Timeline"
aria-label='{title}'
class='wrn-timeline {class}'
>
{#if title || description}
<header class="wrn-timeline__header">
{#if title}
<h3 class="wrn-timeline__title">{title}</h3>
{/if}
{#if description}
<p class="wrn-timeline__description">{description}</p>
{/if}
</header>
{/if}
<ol class="wrn-timeline__list">
{#each items as item, index}
<li
class="wrn-timeline__item"
@click='output.select({ item: item, index: index })'
>
<span
class="wrn-timeline__marker"
data-status='{item.status || "default"}'
>
<span class='{item.icon || "icon-[lucide--circle]"}' aria-hidden="true"></span>
</span>
<article
class="wrn-timeline__card"
data-variant='{variant}'
>
<div class="wrn-timeline__card-header">
<div>
<h4 class="wrn-timeline__item-title">{item.title || item.label}</h4>
{#if item.subtitle}
<p class="wrn-timeline__subtitle">{item.subtitle}</p>
{/if}
</div>
{#if item.date || item.meta}
<time class="wrn-timeline__time">{item.date || item.meta}</time>
{/if}
</div>
{#if item.description}
<p class="wrn-timeline__copy">{item.description}</p>
{/if}
{#if item.href}
<a href='{item.href}' class="wrn-timeline__action">
<span>{item.actionLabel || "View details"}</span>
<span class="icon-[lucide--arrow-right] wrn-timeline__action-icon" aria-hidden="true"></span>
</a>
{/if}
</article>
</li>
{:empty}
<li class="wrn-timeline__empty">No timeline items available.</li>
{/each}
</ol>
<slot></slot>
</section>
}
style {
.wrn-timeline { width: 100%; }
.wrn-timeline__header { margin-bottom: 1.5rem; }
.wrn-timeline__title { color: var(--wrn-color-text); font-size: 1.25rem; font-weight: 700; }
.wrn-timeline__description { max-width: 42rem; margin-top: 0.5rem; color: var(--wrn-color-text-muted); line-height: 1.75rem; }
.wrn-timeline__list { position: relative; margin-left: 1rem; border-left: 1px solid var(--wrn-color-border); }
.wrn-timeline__item { position: relative; padding: 0 0 2rem 2rem; cursor: pointer; }
.wrn-timeline__item:last-child { padding-bottom: 0; }
.wrn-timeline__marker {
position: absolute; top: 0; left: -1.05rem; display: flex; width: 2rem; height: 2rem;
align-items: center; justify-content: center; border: 4px solid var(--wrn-color-background);
border-radius: 50%; color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); box-shadow: var(--wrn-shadow-1);
}
.wrn-timeline__marker:is([data-status="complete"], [data-status="success"]) { background: var(--wrn-color-success); }
.wrn-timeline__marker:is([data-status="warning"], [data-status="pending"]) { background: var(--wrn-color-warning); }
.wrn-timeline__marker:is([data-status="danger"], [data-status="failed"]) { background: var(--wrn-color-danger); }
.wrn-timeline__card { padding: 1.25rem; 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-timeline__card[data-variant="minimal"] { padding: 0; border-color: transparent; background: transparent; box-shadow: none; }
.wrn-timeline__card-header { display: flex; flex-direction: column; gap: 0.5rem; }
.wrn-timeline__item-title { color: var(--wrn-color-text); font-weight: 700; }
.wrn-timeline__subtitle { margin-top: 0.25rem; color: var(--wrn-color-primary); font-size: 0.875rem; font-weight: 500; }
.wrn-timeline__time { flex: none; color: var(--wrn-color-text-muted); font-size: 0.875rem; }
.wrn-timeline__copy { margin-top: 0.75rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
.wrn-timeline__action { display: inline-flex; align-items: center; gap: 0.5rem; margin-top: 1rem; color: var(--wrn-color-primary); font-size: 0.875rem; font-weight: 600; }
.wrn-timeline__action:hover { color: var(--wrn-color-primary-hover); }
.wrn-timeline__action-icon { width: 1rem; height: 1rem; }
.wrn-timeline__empty { padding-left: 2rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; }
@media (min-width: 40rem) {
.wrn-timeline__card-header { flex-direction: row; align-items: flex-start; justify-content: space-between; }
}
}
}
-540
View File
@@ -1,540 +0,0 @@
component ToggleCount {
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)
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
variant: string = "segmented"
class: string = ""
name: string = "billing-cycle"
value: string = "monthly"
firstValue: string = "monthly"
firstLabel: string = "Monthly"
secondValue: string = "annual"
secondLabel: string = "Annual"
ariaLabel: string = "Billing frequency"
items: unknown[] = []
currency: string = "$"
suffix: string = ""
firstValueKey: string = "monthly"
secondValueKey: string = "annual"
emptyValue: string = "—"
align: string = "end"
fullWidth: boolean = true
disabled: boolean = false
animate: boolean = true
animationDuration: number = 450
animationSteps: number = 18
}
state selectedValue = value
functions {
shared function isFirstSelected() {
return selectedValue === firstValue
}
shared function isSecondSelected() {
return selectedValue === secondValue
}
shared function displayValue(item) {
if (isSecondSelected()) {
return item[secondValueKey] !== undefined
? item[secondValueKey]
: emptyValue
}
return item[firstValueKey] !== undefined
? item[firstValueKey]
: emptyValue
}
client function dispatchToggleEvent(sourceEvent, previousValue, payload) {
payload = {
component: "ToggleCount",
name: name,
value: selectedValue,
previousValue: previousValue,
firstValue: firstValue,
secondValue: secondValue
}
output.change(payload)
output.toggle(payload)
}
client function selectValue(sourceEvent, nextValue, previousValue) {
if (disabled || selectedValue === nextValue) {
return
}
previousValue = selectedValue
selectedValue = nextValue
if (
animate &&
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
animateDisplayedValues(sourceEvent, previousValue)
}
dispatchToggleEvent(sourceEvent, previousValue)
}
client function animateDisplayedValues(sourceEvent, previousValue, root, nodes, animationToken) {
root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")
if (!root) {
return
}
animationToken = String(Date.now()) + ":" + String(selectedValue)
root.setAttribute("data-animation-token", animationToken)
nodes = root.querySelectorAll("[data-toggle-count-value]")
animateValueAt(nodes, 0, previousValue, root, animationToken)
}
client function animateValueAt(
nodes,
index,
previousValue,
root,
animationToken,
node,
fromValue,
toValue
) {
if (index >= nodes.length) {
return
}
node = nodes[index]
fromValue = Number(
previousValue === secondValue
? node.getAttribute("data-second-value")
: node.getAttribute("data-first-value")
)
toValue = Number(
selectedValue === secondValue
? node.getAttribute("data-second-value")
: node.getAttribute("data-first-value")
)
if (!Number.isNaN(fromValue) && !Number.isNaN(toValue)) {
node.replaceChildren(String(fromValue))
scheduleValueFrame(
node,
fromValue,
toValue,
1,
Math.max(1, Number(animationSteps) || 1),
Math.max(1, Number(animationDuration) || 1),
root,
animationToken
)
}
animateValueAt(
nodes,
index + 1,
previousValue,
root,
animationToken
)
}
client function scheduleValueFrame(
node,
fromValue,
toValue,
frame,
totalFrames,
duration,
root,
animationToken,
nextValue
) {
if (frame > totalFrames) {
return
}
nextValue = Math.round(
fromValue +
(toValue - fromValue) * (frame / totalFrames)
)
setTimeout(
applyAnimatedValue,
Math.max(1, duration * frame / totalFrames),
node,
nextValue,
root,
animationToken
)
scheduleValueFrame(
node,
fromValue,
toValue,
frame + 1,
totalFrames,
duration,
root,
animationToken
)
}
client function applyAnimatedValue(
node,
nextValue,
root,
animationToken
) {
if (
root &&
root.getAttribute("data-animation-token") !== animationToken
) {
return
}
node.replaceChildren(String(nextValue))
}
client function toggleValue(sourceEvent) {
selectValue(
sourceEvent,
isFirstSelected() ? secondValue : firstValue
)
}
}
view {
<section
{...attrs}
data-wrn-toggle-count
data-variant="{variant}"
data-value="{selectedValue}"
data-disabled="{disabled ? 'true' : 'false'}"
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--toggle-count wrn-next--toggle-count-{variant} {fullWidth ? 'wrn-next--toggle-count-full' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
>
<input type="hidden" name="{name}" value="{selectedValue}" />
<div class="wrn-next__toggle-count-control wrn-next__toggle-count-control--{align}">
{#if variant === "switch"}
<span data-selected="{isFirstSelected() ? 'true' : 'false'}">{firstLabel}</span>
<button
type="button"
role="switch"
aria-label="{ariaLabel}"
aria-checked="{isSecondSelected() ? 'true' : 'false'}"
disabled="{disabled}"
@click="toggleValue(event)"
class="wrn-next__toggle-count-switch"
>
<span aria-hidden="true"></span>
</button>
<span data-selected="{isSecondSelected() ? 'true' : 'false'}">{secondLabel}</span>
{:else}
<div class="wrn-next__toggle-count-segmented" role="group" aria-label="{ariaLabel}">
<button
type="button"
aria-pressed="{isFirstSelected() ? 'true' : 'false'}"
disabled="{disabled}"
@click="selectValue(event, firstValue)"
>{firstLabel}</button>
<button
type="button"
aria-pressed="{isSecondSelected() ? 'true' : 'false'}"
disabled="{disabled}"
@click="selectValue(event, secondValue)"
>{secondLabel}</button>
</div>
{/if}
</div>
{#if items.length > 0}
<div class="wrn-next__toggle-count-items">
{#each items as item}
<article>
<span>{item.label || item.name}</span>
<strong>
{#if currency}<small>{currency}</small>{/if}
<span
data-toggle-count-value
data-first-value="{item[firstValueKey]}"
data-second-value="{item[secondValueKey]}"
>{displayValue(item)}</span>
{#if suffix}<small>{suffix}</small>{/if}
</strong>
{#if item.description}<small>{item.description}</small>{/if}
</article>
{/each}
</div>
{/if}
</section>
}
style {
.wrn-next__toggle-count-control--start {
justify-content: flex-start;
}
.wrn-next__toggle-count-control--center {
justify-content: center;
}
.wrn-next__toggle-count-control--end {
justify-content: flex-end;
}
.wrn-next--toggle-count {
--wrn-toggle-count-accent: var(--wrn-component-color);
display: grid;
width: fit-content;
gap: 0.75rem;
color: var(--wrn-color-text);
}
.wrn-next--toggle-count-full {
width: 100%;
}
.wrn-next__toggle-count-control {
display: flex;
}
.wrn-next__toggle-count-segmented {
display: inline-flex;
gap: 0.2rem;
padding: 0.2rem;
border-radius: calc(var(--wrn-radius-sm) + 0.15rem);
background: var(--wrn-color-surface-2);
box-shadow: inset 0 0 0 1px var(--wrn-color-border);
}
.wrn-next__toggle-count-segmented button {
min-height: 2.35rem;
padding: 0.45rem 0.8rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: var(--wrn-color-muted);
font: inherit;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
}
.wrn-next__toggle-count-segmented button[aria-pressed="true"] {
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
box-shadow:
var(--wrn-shadow-1),
inset 0 -2px var(--wrn-toggle-count-accent);
}
.wrn-next__toggle-count-segmented button:focus-visible,
.wrn-next__toggle-count-switch:focus-visible {
outline: 2px solid var(--wrn-toggle-count-accent);
outline-offset: 2px;
}
.wrn-next__toggle-count-segmented button:disabled,
.wrn-next__toggle-count-switch:disabled {
cursor: not-allowed;
}
.wrn-next__toggle-count-control:has(.wrn-next__toggle-count-switch) {
align-items: center;
gap: 0.7rem;
font-size: 0.78rem;
font-weight: 700;
}
.wrn-next__toggle-count-control > span {
color: var(--wrn-color-muted);
}
.wrn-next__toggle-count-control > span[data-selected="true"] {
color: var(--wrn-color-text);
}
.wrn-next__toggle-count-switch {
position: relative;
width: 2.65rem;
height: 1.45rem;
padding: 0.15rem;
border: 1px solid var(--wrn-color-border);
border-radius: 999px;
background: var(--wrn-color-surface-2);
cursor: pointer;
transition: background-color var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next__toggle-count-switch > span {
display: block;
width: 1rem;
height: 1rem;
border-radius: 50%;
background: var(--wrn-color-text);
box-shadow: var(--wrn-shadow-1);
transition: transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next__toggle-count-switch[aria-checked="true"] {
border-color: var(--wrn-toggle-count-accent);
background: var(--wrn-toggle-count-accent);
}
.wrn-next__toggle-count-switch[aria-checked="true"] > span {
background: white;
transform: translateX(1.15rem);
}
.wrn-next__toggle-count-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
overflow: hidden;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
}
.wrn-next__toggle-count-items article {
display: grid;
align-content: start;
gap: 0.35rem;
min-width: 0;
padding: 1rem;
}
.wrn-next__toggle-count-items article + article {
border-left: 1px solid var(--wrn-color-border);
}
.wrn-next__toggle-count-items article > span {
color: var(--wrn-color-muted);
font-size: 0.75rem;
font-weight: 600;
}
.wrn-next__toggle-count-items strong {
display: flex;
align-items: baseline;
gap: 0.2rem;
font-size: 1.5rem;
line-height: 1;
}
.wrn-next__toggle-count-items [data-toggle-count-value] {
min-width: 2ch;
font-variant-numeric: tabular-nums;
transition:
color var(--wrn-motion-fast) var(--wrn-ease-standard),
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next__toggle-count-items strong small {
color: inherit;
font-size: 0.65em;
}
.wrn-next__toggle-count-items article > small {
color: var(--wrn-color-muted);
font-size: 0.68rem;
}
@media (max-width: 36rem) {
.wrn-next__toggle-count-control {
justify-content: center;
}
.wrn-next__toggle-count-items {
grid-template-columns: 1fr;
}
.wrn-next__toggle-count-items article + article {
border-top: 1px solid var(--wrn-color-border);
border-left: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.wrn-next__toggle-count-items [data-toggle-count-value] {
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;
}
}
}
-880
View File
@@ -1,880 +0,0 @@
component TogglePassword {
outputs {
input(payload: { name?: string; value: string | number | boolean | null | object; visible: boolean })
change(payload: { name?: string; value: string | number | boolean | null | object; visible: boolean })
toggle(payload: { visible: boolean })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Password"
name: string = "password"
value: string = ""
placeholder: string = "Enter your password"
autocomplete: string = "current-password"
minlength: string = ""
maxlength: string = ""
pattern: string = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"
fields: unknown[] = []
visible: boolean = false
toggleable: boolean = true
toggleMode: string = "button"
checkboxLabel: string = "Show password"
showLabel: string = "Show password"
hideLabel: string = "Hide password"
disabled: boolean = false
readonly: boolean = false
required: boolean = false
invalid: boolean = false
helpText: string = ""
validationMessage: string = ""
cornerHint: string = ""
cornerHref: string = ""
class: string = ""
}
state revealed = visible
functions {
shared function toggleVisibility() {
if (disabled || readonly || !toggleable) {
return
}
revealed = !revealed
}
shared function fieldId(field, index) {
return field.id || field.name || name + "-" + index
}
shared function fieldName(field, index) {
return field.name || name + "-" + index
}
}
view {
<fieldset
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--toggle-password {invalid ? 'wrn-next--invalid' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
data-wrn-toggle-password
data-visible="{revealed ? 'true' : 'false'}"
>
{#if fields.length}
<div class="wrn-next__password-fields">
{#each fields as field, index}
<div class="wrn-next__password-field">
<label for="{fieldId(field, index)}">{field.label || label}</label>
<div class="wrn-next__password-control">
<input
id="{fieldId(field, index)}"
type="{revealed ? 'text' : 'password'}"
name="{fieldName(field, index)}"
value="{field.value || ''}"
placeholder="{field.placeholder || placeholder}"
autocomplete="{field.autocomplete || autocomplete}"
minlength="{field.minlength || minlength}"
maxlength="{field.maxlength || maxlength}"
pattern="{field.pattern || pattern}"
disabled="{disabled || field.disabled}"
readonly="{readonly || field.readonly}"
required="{required || field.required}"
aria-invalid="{invalid ? 'true' : 'false'}"
@input="output.input({ name: event.target.name, value: event.target.value, visible: revealed })"
@change="output.change({ name: event.target.name, value: event.target.value, visible: revealed })"
/>
{#if toggleable && toggleMode === "button"}
<button
type="button"
class="wrn-next__password-toggle"
aria-label="{revealed ? hideLabel : showLabel}"
title="{revealed ? hideLabel : showLabel}"
aria-pressed="{revealed ? 'true' : 'false'}"
disabled="{disabled || readonly}"
@click="toggleVisibility(); output.toggle({ visible: revealed })"
>
<span class="wrn-next__password-icon wrn-next__password-icon--show" aria-hidden="true"></span>
</button>
{/if}
</div>
</div>
{/each}
</div>
{:else}
<div class="wrn-next__password-heading">
<label for="{name}-password">{label}</label>
{#if cornerHint}
{#if cornerHref}<a href="{cornerHref}">{cornerHint}</a>{:else}<span>{cornerHint}</span>{/if}
{/if}
</div>
<div class="wrn-next__password-control">
<input
{...attrs}
id="{name}-password"
type="{revealed ? 'text' : 'password'}"
name="{name}"
value="{value}"
placeholder="{placeholder}"
autocomplete="{autocomplete}"
minlength="{minlength}"
maxlength="{maxlength}"
pattern="{pattern}"
disabled="{disabled}"
readonly="{readonly}"
required="{required}"
aria-invalid="{invalid ? 'true' : 'false'}"
aria-describedby="{validationMessage ? name + '-validation' : helpText ? name + '-help' : ''}"
@input="output.input({ value: event.target.value, visible: revealed })"
@change="output.change({ value: event.target.value, visible: revealed })"
/>
{#if toggleable && toggleMode === "button"}
<button
type="button"
class="wrn-next__password-toggle"
aria-label="{revealed ? hideLabel : showLabel}"
title="{revealed ? hideLabel : showLabel}"
aria-pressed="{revealed ? 'true' : 'false'}"
disabled="{disabled || readonly}"
@click="toggleVisibility(); output.toggle({ visible: revealed })"
>
<span class="wrn-next__password-icon wrn-next__password-icon--show" aria-hidden="true"></span>
</button>
{/if}
</div>
{/if}
{#if toggleable && toggleMode === "checkbox"}
<label class="wrn-next__password-checkbox">
<input
type="checkbox"
checked="{revealed}"
disabled="{disabled || readonly}"
@change="revealed = event.target.checked; output.toggle({ visible: revealed })"
/>
<span>{checkboxLabel}</span>
</label>
{/if}
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wrn-next__validation"
data-error="{name}"
>{validationMessage}</small>
</fieldset>
}
style {
.wrn-next--toggle-password {
display: grid;
width: 100%;
min-width: 0;
max-width: none;
box-sizing: border-box;
gap: 0.45rem;
padding: 0;
border: 0;
}
.wrn-next--toggle-password > label {
color: var(--wrn-color-text);
font-size: 0.75rem;
font-weight: 600;
}
.wrn-next__password-heading {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 1rem;
color: var(--wrn-color-text);
font-size: 0.75rem;
font-weight: 600;
}
.wrn-next__password-heading :is(a, span) {
color: var(--wrn-component-color);
font-size: 0.75rem;
font-weight: 500;
text-decoration: none;
}
.wrn-next__password-heading a:hover {
text-decoration: underline;
}
.wrn-next__password-fields {
display: grid;
gap: 1rem;
}
.wrn-next__password-field {
display: grid;
gap: 0.45rem;
}
.wrn-next__password-field > label {
color: var(--wrn-color-text);
font-size: 0.82em;
font-weight: 750;
}
.wrn-next__password-control {
position: relative;
display: flex;
min-width: 0;
align-items: center;
width: 100%;
}
.wrn-next__password-control > input {
width: 100%;
min-width: 0;
min-height: 2.75em;
padding: 0.65em 3em 0.65em 0.8em;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
outline: 0;
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font: inherit;
font-size: 0.8125rem;
transition:
border-color var(--wrn-motion-base) var(--wrn-ease-standard),
box-shadow var(--wrn-motion-base) var(--wrn-ease-standard);
}
.wrn-next__password-control > input:focus-visible {
border-color: var(--wrn-component-color);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 18%, transparent);
}
.wrn-next__password-toggle {
position: absolute;
top: 50%;
right: 0.45em;
display: grid;
width: 2em;
height: 2em;
padding: 0;
place-items: center;
border: 0;
border-radius: calc(var(--wrn-radius-sm) * 0.72);
color: var(--wrn-color-muted);
background: transparent;
cursor: pointer;
transform: translateY(-50%);
}
.wrn-next__password-toggle:hover,
.wrn-next__password-toggle:focus-visible {
color: var(--wrn-component-color);
background: color-mix(in srgb, var(--wrn-component-color) 10%, transparent);
outline: 0;
}
.wrn-next__password-icon {
display: block;
width: 1.05em;
height: 1.05em;
background: currentColor;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.wrn-next__password-icon--show {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.06 12.35a1 1 0 0 1 0-.7C3.7 7.6 7.64 5 12 5c4.36 0 8.3 2.6 9.94 6.65a1 1 0 0 1 0 .7C20.3 16.4 16.36 19 12 19c-4.36 0-8.3-2.6-9.94-6.65Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
}
.wrn-next__password-toggle[aria-pressed="true"]::after {
position: absolute;
width: 1.25em;
height: 0.12em;
border-radius: 999px;
background: currentColor;
content: "";
transform: rotate(45deg);
box-shadow: 0 0 0 1px var(--wrn-color-bg);
}
.wrn-next__password-checkbox {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 0.55rem;
color: var(--wrn-color-muted);
font-size: 0.78em;
cursor: pointer;
}
.wrn-next__password-checkbox > input {
width: 1rem;
height: 1rem;
margin: 0;
accent-color: var(--wrn-component-color);
}
.wrn-next--toggle-password > small {
color: var(--wrn-color-muted);
font-size: 0.72em;
}
/* 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);
}
}
}
-103
View File
@@ -1,103 +0,0 @@
component TreeView {
outputs {
select(payload: { item: object; value: string; sourceEvent?: Event })
toggle(payload: { item: object; value: string; expanded: boolean; sourceEvent?: Event })
expand(payload: { item: object; value: string; sourceEvent?: Event })
collapse(payload: { item: object; value: string; sourceEvent?: Event })
}
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 = ""
}
state expandedValues = defaultExpanded
state selectedValue = selected
functions {
shared function nodeValue(item, index, parent) { return String(item[valueKey] || item.id || parent + index) }
shared function isExpanded(value) { return expandedValues.includes(value) }
client function toggleNode(item, value, sourceEvent, next, expanded) {
next = expandedValues.slice()
expanded = !next.includes(value)
if (expanded) {
next.push(value)
} else {
next = next.filter(function (entry) { return entry !== value })
}
expandedValues = next
output.toggle({ item: item, value: value, expanded: expanded, sourceEvent: sourceEvent })
if (expanded) {
output.expand({ item: item, value: value, sourceEvent: sourceEvent })
} else {
output.collapse({ item: item, value: value, sourceEvent: sourceEvent })
}
}
client function selectNode(item, value, sourceEvent) {
selectedValue = value
output.select({ item: item, value: value, sourceEvent: sourceEvent })
}
}
view {
<section {...attrs} data-ui-component="TreeView" class='wrn-tree-view {class}'>
{#if title}<strong class="wrn-tree-view__title">{title}</strong>{/if}
{#if description}<p class="wrn-tree-view__description">{description}</p>{/if}
<div class="wrn-tree-view__tree" role="tree" aria-label='{title}' data-wrn-roving="vertical">
{#each items as item, index}
<div class="wrn-tree-view__branch" role="none">
<div class="wrn-tree-view__row" role="treeitem" aria-level="1" aria-expanded='{item.children && item.children.length ? isExpanded(nodeValue(item, index, "root-")) : ""}' aria-selected='{selectedValue === nodeValue(item, index, "root-") ? "true" : "false"}'>
{#if item.children && item.children.length}
<button type="button" class="wrn-tree-view__toggle" aria-label='{isExpanded(nodeValue(item, index, "root-")) ? "Collapse " + item[labelKey] : "Expand " + item[labelKey]}' @click='toggleNode(item, nodeValue(item, index, "root-"), event)'><span class="icon-[lucide--chevron-right]" aria-hidden="true"></span></button>
{:else}<span class="wrn-tree-view__toggle" aria-hidden="true"></span>{/if}
<button type="button" class="wrn-tree-view__node" data-wrn-roving-item @click='selectNode(item, nodeValue(item, index, "root-"), event)'>
{#if item.icon}<span class='{item.icon}' aria-hidden="true"></span>{/if}<span>{item[labelKey]}</span>
</button>
</div>
{#if item.children && item.children.length}
<div class="wrn-tree-view__group" role="group" data-show='isExpanded(nodeValue(item, index, "root-"))'>
{#each item.children as child, childIndex}
<div class="wrn-tree-view__row" role="treeitem" aria-level="2" aria-selected='{selectedValue === nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-") ? "true" : "false"}'>
<span class="wrn-tree-view__toggle" aria-hidden="true"></span>
<button type="button" class="wrn-tree-view__node" data-wrn-roving-item @click='selectNode(child, nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-"), event)'>
{#if child.icon}<span class='{child.icon}' aria-hidden="true"></span>{/if}<span>{child[labelKey]}</span>
</button>
</div>
{/each}
</div>
{/if}
</div>
{:empty}<p class="wrn-tree-view__empty">No tree items available.</p>{/each}
</div>
<slot />
</section>
}
style {
.wrn-tree-view { display: grid; gap: 0.5rem; color: var(--wrn-color-text); }
.wrn-tree-view__title { font-size: 0.95rem; }
.wrn-tree-view__description, .wrn-tree-view__empty { margin: 0; color: var(--wrn-color-muted); font-size: 0.8rem; }
.wrn-tree-view__tree { display: grid; gap: 0.15rem; padding: 0.4rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); background: var(--wrn-color-surface); }
.wrn-tree-view__row { display: flex; min-width: 0; align-items: center; gap: 0.2rem; border-radius: var(--wrn-radius-sm); }
.wrn-tree-view__row[aria-selected="true"] { background: var(--wrn-color-primary-soft); }
.wrn-tree-view__toggle { display: inline-grid; width: 1.8rem; height: 1.8rem; flex: none; padding: 0; place-items: center; border: 0; border-radius: 0.35rem; color: var(--wrn-color-muted); background: transparent; }
button.wrn-tree-view__toggle { cursor: pointer; }
.wrn-tree-view__toggle > span { width: 0.9rem; height: 0.9rem; transition: transform var(--wrn-motion-fast); }
.wrn-tree-view__row[aria-expanded="true"] > .wrn-tree-view__toggle > span { transform: rotate(90deg); }
.wrn-tree-view__node { display: flex; min-width: 0; flex: 1; align-items: center; gap: 0.45rem; padding: 0.45rem 0.5rem; border: 0; border-radius: var(--wrn-radius-sm); color: inherit; background: transparent; font: inherit; text-align: left; cursor: pointer; }
.wrn-tree-view__node:hover, button.wrn-tree-view__toggle:hover { background: var(--wrn-color-surface-2); }
.wrn-tree-view__node:focus-visible, button.wrn-tree-view__toggle:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: -2px; }
.wrn-tree-view__node > [class*="icon-"] { width: 1rem; height: 1rem; flex: none; color: var(--wrn-color-primary); }
.wrn-tree-view__group { display: grid; margin-left: 1.8rem; padding-left: 0.45rem; border-left: 1px solid var(--wrn-color-border); }
}
}
-123
View File
@@ -1,123 +0,0 @@
component Typography {
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
class: string = ""
}
view {
<div
data-ui-component="Typography"
class='wrn-typography {class}'
data-size='{size}'
data-columns='{columns}'
data-gap='{gap}'
data-max-width='{maxWidth}'
>
<slot></slot>
</div>
}
style {
.wrn-typography {
color: var(--wrn-color-text);
font-size: 1rem;
}
.wrn-typography[data-size="sm"] {
font-size: 0.875rem;
}
.wrn-typography[data-size="lg"] {
font-size: 1.125rem;
}
.wrn-typography[data-max-width="md"] {
max-width: 48rem;
}
.wrn-typography[data-max-width="lg"] {
max-width: 64rem;
}
.wrn-typography[data-max-width="xl"] {
max-width: 80rem;
}
.wrn-typography[data-max-width="full"] {
max-width: none;
}
/*
* Multi-column prose only below a comfortable reading width; a narrow
* screen split into columns is unreadable.
*/
.wrn-typography[data-gap="sm"] {
column-gap: 1rem;
}
.wrn-typography[data-gap="md"] {
column-gap: 2rem;
}
.wrn-typography[data-gap="lg"] {
column-gap: 3rem;
}
@media (min-width: 768px) {
.wrn-typography[data-columns="2"] {
column-count: 2;
}
}
@media (min-width: 1024px) {
.wrn-typography[data-columns="3"] {
column-count: 3;
}
}
.wrn-typography :where(h1, h2, h3, h4) {
color: var(--wrn-color-text);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.2;
break-after: avoid;
}
.wrn-typography :where(h1) { font-size: clamp(2rem, 4vw, 3.5rem); margin: 0 0 1.5rem; }
.wrn-typography :where(h2) { font-size: clamp(1.5rem, 3vw, 2.25rem); margin: 2.5rem 0 1rem; }
.wrn-typography :where(h3) { font-size: 1.35rem; margin: 2rem 0 0.75rem; }
.wrn-typography :where(p, ul, ol, blockquote, pre, table) { margin: 1rem 0; }
.wrn-typography :where(p, li) { color: var(--wrn-color-text-muted); line-height: 1.8; }
.wrn-typography :where(a) { color: var(--wrn-color-primary); font-weight: 600; text-underline-offset: 0.2em; }
.wrn-typography :where(a:hover) { color: var(--wrn-color-primary-hover); text-decoration: underline; }
.wrn-typography :where(ul, ol) { padding-left: 1.4rem; }
.wrn-typography :where(ul) { list-style: disc; }
.wrn-typography :where(ol) { list-style: decimal; }
.wrn-typography :where(blockquote) {
border-left: 4px solid var(--wrn-color-primary);
background: var(--wrn-color-primary-soft);
border-radius: 0 0.75rem 0.75rem 0;
padding: 1rem 1.25rem;
color: var(--wrn-color-text);
}
.wrn-typography :where(code) {
border-radius: 0.35rem;
background: var(--wrn-color-surface-soft);
padding: 0.15rem 0.35rem;
font-size: 0.9em;
}
.wrn-typography :where(pre) {
overflow-x: auto;
border: 1px solid var(--wrn-color-border);
border-radius: 1rem;
background: var(--wrn-color-surface-raised);
padding: 1rem;
}
.wrn-typography :where(img) { border-radius: 1rem; }
.wrn-typography :where(hr) { border-color: var(--wrn-color-border); margin: 2rem 0; }
}
}
-110
View File
@@ -1,110 +0,0 @@
component WysiwygEditor {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Wysiwyg Editor"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--wysiwyg-editor wrn-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
style {
.wrn-next--wysiwyg-editor {
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--wysiwyg-editor > p { margin: 0; color: var(--wrn-color-muted); }
.wrn-next--wysiwyg-editor > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-next--wysiwyg-editor > .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;
}
}
}
-462
View File
@@ -1,462 +0,0 @@
component Alert {
outputs {
dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "info"
variant: string = "soft"
class: string = ""
radius: string = "md"
shadow: string = "sm"
title: string = "Alert"
description: string = ""
items: unknown[] = []
actions: unknown[] = []
showIcon: boolean = false
icon: string = ""
dismissible: boolean = false
dismissLabel: string = "Dismiss alert"
role: string = "alert"
live: string = "polite"
linkLabel: string = ""
linkHref: string = ""
actionLabel: string = ""
actionHref: string = ""
compact: boolean = false
}
state visible: boolean = true
functions {
// The output name has to be written out rather than computed: outputs are
// resolved as named properties, so a dynamic key would not reach a parent
// binding. Only two names exist here, so a branch is honest and cheap.
client function dispatchAlertEvent(sourceEvent, eventName, action, payload) {
payload = {
component: "Alert",
title: title,
color: color,
variant: variant,
action: action
}
if (eventName === "dismiss") {
output.dismiss(payload)
} else {
output.action(payload)
}
}
client function dismissAlert(sourceEvent) {
visible = false
dispatchAlertEvent(sourceEvent, "dismiss", null)
}
client function selectAction(sourceEvent, action) {
dispatchAlertEvent(sourceEvent, "action", action)
}
}
view {
<section
{...attrs}
data-wrn-alert
data-color="{color}"
data-variant="{variant}"
data-radius="{radius}"
data-shadow="{shadow}"
data-show="visible"
aria-hidden="{visible ? 'false' : 'true'}"
role="{role}"
aria-live="{live}"
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--alert wrn-next--alert-{variant} {compact ? 'wrn-next--alert-compact' : ''} {class}"
>
{#if showIcon}
<span class="wrn-next__alert-icon" aria-hidden="true">
{#if icon}
<span class="{icon}"></span>
{:else if color === "success"}
<span class="icon-[lucide--circle-check]"></span>
{:else if color === "danger"}
<span class="icon-[lucide--circle-x]"></span>
{:else if color === "warning"}
<span class="icon-[lucide--triangle-alert]"></span>
{:else}
<span class="icon-[lucide--info]"></span>
{/if}
</span>
{/if}
<div class="wrn-next__alert-body">
{#if title}<strong class="wrn-next__alert-title">{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items.length > 0}
<ul>
{#each items as item}<li>{item.label || item}</li>{/each}
</ul>
{/if}
{#if actions.length > 0 || actionLabel || linkLabel}
<div class="wrn-next__alert-actions">
{#each actions as item}
<a
href="{item.href || '#'}"
data-variant="{item.variant || 'link'}"
@click="selectAction(event, item)"
>{item.label}</a>
{/each}
{#if actionLabel}
<a
href="{actionHref || '#'}"
data-variant="action"
@click="selectAction(event, { label: actionLabel, href: actionHref })"
>{actionLabel}</a>
{/if}
{#if linkLabel}
<a
href="{linkHref || '#'}"
data-variant="link"
@click="selectAction(event, { label: linkLabel, href: linkHref })"
>{linkLabel}</a>
{/if}
</div>
{/if}
</div>
{#if dismissible}
<button
type="button"
class="wrn-next__alert-dismiss"
aria-label="{dismissLabel}"
@click="dismissAlert(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</section>
}
style {
.wrn-next--alert-soft {
border-color: color-mix(in srgb, var(--wrn-alert-color) 45%, var(--wrn-color-border));
background: var(--wrn-alert-soft);
}
.wrn-next--alert-soft .wrn-next__alert-title,
.wrn-next--alert-soft .wrn-next__alert-icon,
.wrn-next--alert-soft .wrn-next__alert-actions a {
color: var(--wrn-alert-color);
}
.wrn-next--alert-solid {
border-color: var(--wrn-alert-color);
background: var(--wrn-alert-color);
color: white;
}
.wrn-next--alert.wrn-next--alert-solid
:is(
.wrn-next__alert-title,
.wrn-next__alert-body > p,
.wrn-next__alert-body li,
.wrn-next__alert-actions a,
.wrn-next__alert-icon
) {
color: currentColor;
}
.wrn-next--alert-solid[data-color="light"] {
color: #18181b;
}
.wrn-next--alert-solid[data-color="warning"] {
color: #18181b;
}
.wrn-next--alert-bordered {
border-color: var(--wrn-alert-color);
background: var(--wrn-color-surface);
}
.wrn-next--alert-bordered .wrn-next__alert-title,
.wrn-next--alert-bordered .wrn-next__alert-icon,
.wrn-next--alert-bordered .wrn-next__alert-actions a {
color: var(--wrn-alert-color);
}
.wrn-next--alert-accent {
border-color: color-mix(in srgb, var(--wrn-alert-color) 35%, var(--wrn-color-border));
border-left: 4px solid var(--wrn-alert-color);
background: var(--wrn-alert-soft);
}
.wrn-next--alert-accent .wrn-next__alert-title,
.wrn-next--alert-accent .wrn-next__alert-icon,
.wrn-next--alert-accent .wrn-next__alert-actions a {
color: var(--wrn-alert-color);
}
.wrn-next--alert {
--wrn-alert-color: var(--wrn-component-color);
--wrn-alert-soft: color-mix(in srgb, var(--wrn-alert-color) 16%, var(--wrn-color-surface));
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.8rem;
width: 100%;
padding: 1rem;
border: 1px solid transparent;
border-radius: var(--wrn-radius, 0.75rem);
color: var(--wrn-color-text);
}
.wrn-next--alert[data-radius="none"] {
border-radius: 0;
}
.wrn-next--alert[data-radius="sm"] {
border-radius: var(--wrn-radius-sm, 0.375rem);
}
.wrn-next--alert[data-radius="lg"] {
border-radius: calc(var(--wrn-radius, 0.75rem) * 1.35);
}
.wrn-next--alert[data-radius="xl"] {
border-radius: calc(var(--wrn-radius, 0.75rem) * 1.75);
}
.wrn-next--alert[data-shadow="none"] {
box-shadow: none;
}
.wrn-next--alert[data-shadow="sm"] {
box-shadow: 0 6px 16px color-mix(in srgb, #000 14%, transparent);
}
.wrn-next--alert[data-shadow="md"] {
box-shadow:
0 2px 6px color-mix(in srgb, #000 10%, transparent),
0 12px 28px color-mix(in srgb, #000 18%, transparent);
}
.wrn-next--alert[data-shadow="lg"] {
box-shadow:
0 4px 10px color-mix(in srgb, #000 12%, transparent),
0 20px 48px color-mix(in srgb, #000 24%, transparent);
}
.wrn-next--alert[data-color="secondary"] {
--wrn-alert-color: #737373;
}
.wrn-next--alert[data-color="success"] {
--wrn-alert-color: #0f9f8f;
}
.wrn-next--alert[data-color="danger"] {
--wrn-alert-color: #dc3545;
}
.wrn-next--alert[data-color="warning"] {
--wrn-alert-color: #eab308;
}
.wrn-next--alert[data-color="info"] {
--wrn-alert-color: #2563eb;
}
.wrn-next--alert[data-color="dark"] {
--wrn-alert-color: #27272a;
}
.wrn-next--alert[data-color="light"] {
--wrn-alert-color: #f4f4f5;
}
.wrn-next--alert-compact {
padding: 0.75rem 0.9rem;
}
.wrn-next--alert-compact:not(:has(.wrn-next__alert-icon)):not(:has(.wrn-next__alert-dismiss)) {
grid-template-columns: minmax(0, 1fr);
}
.wrn-next--alert-compact .wrn-next__alert-body {
display: block;
}
.wrn-next--alert-compact .wrn-next__alert-title {
margin-right: 0.25rem;
}
.wrn-next--alert-compact .wrn-next__alert-body > p {
display: inline;
}
.wrn-next__alert-icon {
display: inline-grid;
width: 1.25rem;
height: 1.25rem;
place-items: center;
margin-top: 0.05rem;
color: var(--wrn-alert-color);
}
.wrn-next__alert-icon > span {
width: 1rem;
height: 1rem;
}
.wrn-next__alert-body {
display: grid;
min-width: 0;
gap: 0.35rem;
}
.wrn-next__alert-title {
font-size: 0.88rem;
line-height: 1.45;
}
.wrn-next__alert-body > p,
.wrn-next__alert-body li {
color: inherit;
font-size: 0.8rem;
line-height: 1.55;
}
.wrn-next__alert-body > ul {
display: grid;
gap: 0.25rem;
margin: 0.15rem 0 0;
padding-left: 1.25rem;
}
.wrn-next__alert-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
margin-top: 0.35rem;
}
.wrn-next__alert-actions a {
color: var(--wrn-alert-color);
font-size: 0.78rem;
font-weight: 700;
text-decoration: none;
}
.wrn-next__alert-actions a:hover {
text-decoration: underline;
}
.wrn-next__alert-actions a[data-variant="action"] {
padding: 0.35rem 0.6rem;
border-radius: var(--wrn-radius-sm);
background: var(--wrn-alert-color);
color: white;
text-decoration: none;
}
.wrn-next__alert-dismiss {
display: inline-grid;
width: 1.75rem;
height: 1.75rem;
place-items: center;
padding: 0;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: currentColor;
cursor: pointer;
}
.wrn-next__alert-dismiss:hover {
background: color-mix(in srgb, currentColor 10%, transparent);
}
.wrn-next__alert-dismiss:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (max-width: 36rem) {
.wrn-next--alert {
grid-template-columns: auto minmax(0, 1fr);
}
.wrn-next__alert-dismiss {
grid-column: 2;
grid-row: 1;
justify-self: end;
}
}
/* 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;
}
}
}
-420
View File
@@ -1,420 +0,0 @@
component Avatar {
outputs {
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
src: string = ""
alt: string = ""
initials: string = ""
size: string = "md"
color: string = "primary"
variant: string = "solid"
shape: string = "circle"
status: string = ""
statusLabel: string = ""
statusPosition: string = "bottom"
badge: string = ""
badgeIcon: string = ""
badgeLabel: string = ""
tooltip: string = ""
name: string = ""
description: string = ""
loading: string = "lazy"
class: string = ""
}
view {
<span
{...attrs}
class="wrn-component wrn-next--avatar {description ? 'wrn-next--avatar-media' : ''} {class}"
data-wrn-avatar
>
<span
class="wrn-next__avatar-wrap"
data-shape="{shape}"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-status-position="{statusPosition}"
tabindex="{tooltip ? '0' : '-1'}"
aria-label="{tooltip || name || alt || initials || 'Avatar'}"
>
<span class="wrn-next__avatar">
{#if src}
<img src="{src}" alt="{alt || name}" loading="{loading}" />
{:else if initials}
<span class="wrn-next__avatar-initials" aria-hidden="true">{initials}</span>
{:else}
<span class="wrn-next__avatar-placeholder icon-[lucide--user-round]" aria-hidden="true"></span>
{/if}
</span>
{#if status}
<span
class="wrn-next__avatar-status"
data-status="{status}"
title="{statusLabel || status}"
aria-label="{statusLabel || status}"
></span>
{/if}
{#if badgeIcon}
<span class="wrn-next__avatar-badge" aria-label="{badgeLabel || 'Linked account'}">
<span class="{badgeIcon}" aria-hidden="true"></span>
</span>
{:else if badge}
<span class="wrn-next__avatar-badge wrn-next__avatar-badge--text" aria-label="{badgeLabel || badge}">
{badge}
</span>
{/if}
{#if tooltip}
<span class="wrn-next__avatar-tooltip" role="tooltip">{tooltip}</span>
{/if}
</span>
{#if description}
<span class="wrn-next__avatar-copy">
<strong>{name || alt || initials}</strong>
<span>{description}</span>
</span>
{/if}
</span>
}
style {
/* --- Avatar -------------------------------------------------------------- */
.wrn-next--avatar {
display: inline-flex;
width: fit-content;
max-width: 100%;
align-items: center;
}
.wrn-next--avatar-media {
gap: 0.8rem;
}
.wrn-next__avatar-wrap {
--wrn-avatar-size: 3rem;
--wrn-avatar-color: var(--wrn-color-primary);
position: relative;
display: inline-grid;
width: var(--wrn-avatar-size);
height: var(--wrn-avatar-size);
flex: none;
place-items: center;
border-radius: 999px;
outline: none;
}
.wrn-next__avatar-wrap[data-size="xs"] {
--wrn-avatar-size: 1.75rem;
}
.wrn-next__avatar-wrap[data-size="sm"] {
--wrn-avatar-size: 2.25rem;
}
.wrn-next__avatar-wrap[data-size="md"],
.wrn-next__avatar-wrap[data-size="default"] {
--wrn-avatar-size: 3rem;
}
.wrn-next__avatar-wrap[data-size="lg"] {
--wrn-avatar-size: 4rem;
}
.wrn-next__avatar-wrap[data-size="xl"] {
--wrn-avatar-size: 5rem;
}
.wrn-next__avatar-wrap[data-color="secondary"] {
--wrn-avatar-color: #737373;
}
.wrn-next__avatar-wrap[data-color="success"] {
--wrn-avatar-color: var(--wrn-color-success);
}
.wrn-next__avatar-wrap[data-color="info"] {
--wrn-avatar-color: var(--wrn-color-info);
}
.wrn-next__avatar-wrap[data-color="danger"] {
--wrn-avatar-color: var(--wrn-color-danger);
}
.wrn-next__avatar-wrap[data-color="warning"] {
--wrn-avatar-color: var(--wrn-color-warning);
}
.wrn-next__avatar-wrap[data-color="light"] {
--wrn-avatar-color: #f4f4f5;
}
.wrn-next__avatar-wrap[data-color="dark"] {
--wrn-avatar-color: #27272a;
}
.wrn-next__avatar {
display: grid;
width: 100%;
height: 100%;
overflow: hidden;
place-items: center;
color: white;
border: 1px solid color-mix(in srgb, var(--wrn-avatar-color) 58%, var(--wrn-color-border));
border-radius: inherit;
background: var(--wrn-avatar-color);
box-shadow:
0 0 0 2px var(--wrn-color-bg),
0 5px 14px rgb(0 0 0 / 0.16);
}
.wrn-next__avatar-wrap[data-shape="rounded"] {
border-radius: calc(var(--wrn-radius-sm, 0.5rem) * 0.72);
}
.wrn-next__avatar-wrap[data-shape="square"] {
border-radius: 0;
}
.wrn-next__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.wrn-next__avatar-initials {
font-size: calc(var(--wrn-avatar-size) * 0.32);
font-weight: 750;
line-height: 1;
letter-spacing: -0.02em;
}
.wrn-next__avatar-placeholder {
width: 54%;
height: 54%;
color: currentColor;
opacity: 0.72;
}
.wrn-next__avatar-wrap[data-variant="soft"] .wrn-next__avatar {
color: var(--wrn-avatar-color);
border-color: transparent;
background: color-mix(in srgb, var(--wrn-avatar-color) 15%, var(--wrn-color-surface));
box-shadow: none;
}
.wrn-next__avatar-wrap[data-variant="outline"] .wrn-next__avatar {
color: var(--wrn-avatar-color);
border-color: var(--wrn-avatar-color);
background: transparent;
box-shadow: none;
}
.wrn-next__avatar-wrap[data-variant="white"] .wrn-next__avatar {
color: var(--wrn-color-text);
border-color: var(--wrn-color-border);
background: var(--wrn-color-surface);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
}
.wrn-next__avatar-status {
position: absolute;
right: -0.04rem;
bottom: -0.04rem;
z-index: 2;
width: max(0.58rem, calc(var(--wrn-avatar-size) * 0.24));
height: max(0.58rem, calc(var(--wrn-avatar-size) * 0.24));
border: 2px solid var(--wrn-color-bg);
border-radius: 999px;
background: var(--wrn-color-muted);
}
.wrn-next__avatar-wrap[data-status-position="top"] .wrn-next__avatar-status {
top: -0.04rem;
bottom: auto;
}
.wrn-next__avatar-status[data-status="online"],
.wrn-next__avatar-status[data-status="success"] {
background: #14cba8;
}
.wrn-next__avatar-status[data-status="busy"],
.wrn-next__avatar-status[data-status="danger"] {
background: #fb5b68;
}
.wrn-next__avatar-status[data-status="away"],
.wrn-next__avatar-status[data-status="warning"] {
background: #ffc400;
}
.wrn-next__avatar-badge {
position: absolute;
right: -0.25rem;
bottom: -0.25rem;
z-index: 3;
display: grid;
width: max(1rem, calc(var(--wrn-avatar-size) * 0.42));
height: max(1rem, calc(var(--wrn-avatar-size) * 0.42));
place-items: center;
color: white;
border: 2px solid var(--wrn-color-bg);
border-radius: 999px;
background: #27272a;
box-shadow: 0 4px 10px rgb(0 0 0 / 0.22);
}
.wrn-next__avatar-badge > span {
width: 62%;
height: 62%;
}
.wrn-next__avatar-badge--text {
font-size: calc(var(--wrn-avatar-size) * 0.18);
font-weight: 800;
}
.wrn-next__avatar-tooltip {
position: absolute;
left: 50%;
bottom: calc(100% + 0.65rem);
z-index: 20;
width: max-content;
max-width: 15rem;
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.2);
font-size: 0.75rem;
font-weight: 650;
line-height: 1.25;
pointer-events: none;
opacity: 0;
transform: translate(-50%, 0.25rem);
transition:
opacity var(--wrn-motion-fast) var(--wrn-ease-standard),
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next__avatar-tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
border: 0.3rem solid transparent;
border-top-color: var(--wrn-color-text);
transform: translateX(-50%);
}
.wrn-next__avatar-wrap:hover .wrn-next__avatar-tooltip,
.wrn-next__avatar-wrap:focus-visible .wrn-next__avatar-tooltip {
opacity: 1;
transform: translate(-50%, 0);
}
.wrn-next__avatar-wrap:focus-visible {
outline: 2px solid var(--wrn-avatar-color);
outline-offset: 4px;
}
.wrn-next__avatar-copy {
display: grid;
min-width: 0;
gap: 0.15rem;
}
.wrn-next__avatar-copy strong {
color: var(--wrn-color-text);
font-size: 0.9rem;
}
.wrn-next__avatar-copy > span {
overflow: hidden;
color: var(--wrn-color-muted);
font-size: 0.78rem;
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;
}
}
}
-420
View File
@@ -1,420 +0,0 @@
component Badge {
outputs {
dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
label: string = "Badge"
size: string = "md"
color: string = "primary"
variant: string = "solid"
shape: string = "pill"
class: string = ""
icon: string = ""
iconPosition: string = "start"
dot: boolean = false
dotOnly: boolean = false
dotLabel: string = "Status"
animated: boolean = false
avatarSrc: string = ""
avatarAlt: string = ""
dismissible: boolean = false
dismissLabel: string = "Remove badge"
truncate: boolean = false
maxWidth: string = "12rem"
anchorLabel: string = ""
anchorIcon: string = ""
placement: string = "inline"
anchorLabelText: string = "Badge anchor"
}
state visible: boolean = true
functions {
// output.dismiss reaches a parent @dismiss binding; a raw dispatchEvent on
// the root does not. The runtime registers parent handlers in a registry
// that only the output proxy consults, so the CustomEvent this used to
// build bubbled past every binding and was never seen by anyone.
client function dismissBadge() {
visible = false
output.dismiss({ component: "Badge", label: label })
}
}
view {
<span
{...attrs}
class="wrn-component wrn-next--badge-root {anchorLabel || anchorIcon ? 'wrn-next--badge-anchored' : ''} {class}"
data-wrn-badge
data-placement="{placement}"
data-show="visible"
aria-hidden="{visible ? 'false' : 'true'}"
>
{#if anchorLabel || anchorIcon}
<button type="button" class="wrn-next__badge-anchor" aria-label="{anchorLabelText}">
{#if anchorIcon}<span class="{anchorIcon}" aria-hidden="true"></span>{/if}
{#if anchorLabel}<span>{anchorLabel}</span>{/if}
</button>
{/if}
<span
class="wrn-next__badge"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-shape="{shape}"
data-animated="{animated ? 'true' : 'false'}"
style="--wrn-badge-max-width:{maxWidth}"
role="{dotOnly ? 'status' : ''}"
aria-label="{dotOnly ? dotLabel : ''}"
>
{#if animated}
<span class="wrn-next__badge-ping" aria-hidden="true"></span>
{/if}
{#if avatarSrc}
<img class="wrn-next__badge-avatar" src="{avatarSrc}" alt="{avatarAlt}" loading="lazy" />
{/if}
{#if dot || dotOnly}
<span class="wrn-next__badge-dot" aria-hidden="true"></span>
{/if}
{#if icon && iconPosition === "start"}
<span class="wrn-next__badge-icon {icon}" aria-hidden="true"></span>
{/if}
{#if !dotOnly}
<span class="wrn-next__badge-label {truncate ? 'wrn-next__badge-label--truncate' : ''}">
{label}
</span>
{/if}
{#if icon && iconPosition === "end"}
<span class="wrn-next__badge-icon {icon}" aria-hidden="true"></span>
{/if}
{#if dismissible}
<button type="button" class="wrn-next__badge-dismiss" aria-label="{dismissLabel}" @click="dismissBadge(event)">
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</span>
</span>
}
style {
@keyframes wrn-badge-ping {
75%,
100% {
opacity: 0;
transform: scale(1.45);
}
}
/* --- Badge --------------------------------------------------------------- */
.wrn-next--badge-root {
position: relative;
display: inline-flex;
width: fit-content;
max-width: 100%;
vertical-align: middle;
}
.wrn-next__badge {
--wrn-badge-color: var(--wrn-color-primary);
position: relative;
isolation: isolate;
display: inline-flex;
min-width: 0;
min-height: 1.65rem;
align-items: center;
justify-content: center;
gap: 0.35rem;
max-width: var(--wrn-badge-max-width);
padding: 0.25rem 0.6rem;
color: white;
border: 1px solid var(--wrn-badge-color);
border-radius: 999px;
background: var(--wrn-badge-color);
box-shadow: 0 4px 12px color-mix(in srgb, var(--wrn-badge-color) 18%, transparent);
font-size: 0.75rem;
font-weight: 750;
line-height: 1;
white-space: nowrap;
}
.wrn-next__badge[data-size="xs"] {
min-height: 1.25rem;
padding: 0.15rem 0.4rem;
font-size: 0.625rem;
}
.wrn-next__badge[data-size="sm"] {
min-height: 1.45rem;
padding: 0.2rem 0.5rem;
font-size: 0.6875rem;
}
.wrn-next__badge[data-size="lg"] {
min-height: 2rem;
padding: 0.35rem 0.75rem;
font-size: 0.825rem;
}
.wrn-next__badge[data-color="secondary"] {
--wrn-badge-color: #737373;
}
.wrn-next__badge[data-color="success"] {
--wrn-badge-color: #0f9f8f;
}
.wrn-next__badge[data-color="info"] {
--wrn-badge-color: #2563eb;
}
.wrn-next__badge[data-color="danger"] {
--wrn-badge-color: #ef3340;
}
.wrn-next__badge[data-color="warning"] {
--wrn-badge-color: #eab308;
}
.wrn-next__badge[data-color="dark"] {
--wrn-badge-color: #27272a;
}
.wrn-next__badge[data-color="light"] {
--wrn-badge-color: #f4f4f5;
}
.wrn-next__badge[data-color="warning"][data-variant="solid"],
.wrn-next__badge[data-color="light"][data-variant="solid"] {
color: #18181b;
}
.wrn-next__badge[data-variant="soft"] {
color: var(--wrn-badge-color);
border-color: transparent;
background: color-mix(in srgb, var(--wrn-badge-color) 15%, var(--wrn-color-surface));
box-shadow: none;
}
.wrn-next__badge[data-variant="outline"] {
color: var(--wrn-badge-color);
background: transparent;
box-shadow: none;
}
.wrn-next__badge[data-variant="white"] {
color: var(--wrn-color-text);
border-color: var(--wrn-color-border);
background: var(--wrn-color-surface);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
}
.wrn-next__badge[data-shape="rounded"] {
border-radius: 0.4rem;
}
.wrn-next__badge[data-shape="square"] {
border-radius: 0;
}
.wrn-next__badge-label {
min-width: 0;
}
.wrn-next__badge-label--truncate {
overflow: hidden;
text-overflow: ellipsis;
}
.wrn-next__badge-dot {
width: 0.42rem;
height: 0.42rem;
flex: none;
border-radius: 999px;
background: currentColor;
}
.wrn-next__badge:has(.wrn-next__badge-dot):not(:has(.wrn-next__badge-label)) {
width: 0.7rem;
min-width: 0.7rem;
height: 0.7rem;
min-height: 0.7rem;
padding: 0;
border: 2px solid var(--wrn-color-bg);
}
.wrn-next__badge:has(.wrn-next__badge-dot):not(:has(.wrn-next__badge-label))
.wrn-next__badge-dot {
width: 100%;
height: 100%;
}
.wrn-next__badge-icon {
width: 0.9rem;
height: 0.9rem;
flex: none;
}
.wrn-next__badge-avatar {
width: 1.25rem;
height: 1.25rem;
margin-left: -0.35rem;
object-fit: cover;
border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
border-radius: 999px;
}
.wrn-next__badge-dismiss {
display: grid;
width: 1.15rem;
height: 1.15rem;
margin-right: -0.3rem;
padding: 0;
place-items: center;
color: currentColor;
border: 0;
border-radius: 999px;
background: color-mix(in srgb, currentColor 12%, transparent);
cursor: pointer;
}
.wrn-next__badge-dismiss:hover {
background: color-mix(in srgb, currentColor 22%, transparent);
}
.wrn-next__badge-dismiss:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.wrn-next__badge-dismiss span {
width: 0.75rem;
height: 0.75rem;
}
.wrn-next__badge-anchor {
display: inline-flex;
min-height: 2.75rem;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.65rem 0.9rem;
color: var(--wrn-color-text);
border: 1px solid var(--wrn-color-border);
border-radius: 0.5rem;
background: var(--wrn-color-surface);
font: inherit;
font-size: 0.875rem;
font-weight: 700;
}
.wrn-next__badge-anchor > [class*="icon-"] {
width: 1rem;
height: 1rem;
}
.wrn-next--badge-anchored[data-placement="inline"] {
align-items: center;
}
.wrn-next--badge-anchored[data-placement="inline"] .wrn-next__badge {
margin-left: -0.55rem;
}
.wrn-next--badge-anchored[data-placement="top-right"] .wrn-next__badge {
position: absolute;
top: -0.55rem;
right: -0.75rem;
z-index: 2;
}
.wrn-next__badge-ping {
position: absolute;
z-index: -1;
inset: -0.25rem;
border-radius: inherit;
background: var(--wrn-badge-color);
opacity: 0.5;
animation: wrn-badge-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.wrn-next__badge-ping {
animation: 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;
}
}
}
-467
View File
@@ -1,467 +0,0 @@
component Button {
outputs {
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
label: string = "Button"
loadingLabel: string = "Loading…"
description: string = ""
as: string = ""
href: string = ""
target: string = ""
rel: string = ""
type: string = "button"
variant: string = "default"
color: string = "primary"
size: string = "default"
disabled: boolean = false
loading: boolean = false
pill: boolean = false
fullWidth: boolean = false
icon: string = ""
iconPosition: string = "start"
ariaLabel: string = ""
ariaPressed: string = ""
ariaExpanded: string = ""
ariaControls: string = ""
title: string = ""
autofocus: boolean = false
controlClass: string = ""
class: string = ""
}
view {
<span
class="wrn-action {fullWidth ? 'w-full' : ''} {class}"
>
{#if as === "a" || (as === "" && href)}
<a
{...attrs}
href="{disabled || loading ? '' : href}"
target="{target}"
rel="{rel}"
title="{title}"
aria-label="{ariaLabel || ((size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? label : '')}"
aria-disabled="{disabled || loading}"
aria-busy="{loading}"
aria-pressed="{ariaPressed}"
aria-expanded="{ariaExpanded}"
aria-controls="{ariaControls}"
tabindex="{disabled || loading ? '-1' : '0'}"
class="wrn-btn wrn-btn--variant-{variant} wrn-btn--color-{color} wrn-btn--size-{size} {pill ? 'wrn-btn--pill' : ''} {description ? 'wrn-btn--with-description' : ''} {fullWidth ? 'w-full' : ''} {controlClass}"
>
{#if loading}
<span
class="wrn-spinner wrn-spinner--inline"
aria-hidden="true"
>
</span>
{/if}
{#if icon && !loading && iconPosition === "start"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
<span
class="wrn-btn__copy {(size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? 'wrn-visually-hidden' : ''}"
>
<span
class="wrn-btn__label"
>
<slot>{loading ? loadingLabel : label}</slot>
</span>
{#if description && !loading}
<span
class="wrn-btn__description"
>
{description}
</span>
{/if}
</span>
{#if icon && !loading && iconPosition === "end"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
<span
class="wrn-btn__tooltip"
role="tooltip"
aria-hidden="true"
>
{ariaLabel || label}
</span>
{/if}
</a>
{:else}
<button
{...attrs}
type="{type}"
title="{title}"
autofocus="{autofocus}"
disabled="{disabled || loading}"
aria-label="{ariaLabel || ((size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? label : '')}"
aria-busy="{loading}"
aria-pressed="{ariaPressed}"
aria-expanded="{ariaExpanded}"
aria-controls="{ariaControls}"
class="wrn-btn wrn-btn--variant-{variant} wrn-btn--color-{color} wrn-btn--size-{size} {pill ? 'wrn-btn--pill' : ''} {description ? 'wrn-btn--with-description' : ''} {fullWidth ? 'w-full' : ''} {controlClass}"
>
{#if loading}
<span
class="wrn-spinner wrn-spinner--inline"
aria-hidden="true"
>
</span>
{/if}
{#if icon && !loading && iconPosition === "start"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
<span
class="wrn-btn__copy {(size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? 'wrn-visually-hidden' : ''}"
>
<span
class="wrn-btn__label"
>
<slot>{loading ? loadingLabel : label}</slot>
</span>
{#if description && !loading}
<span
class="wrn-btn__description"
>
{description}
</span>
{/if}
</span>
{#if icon && !loading && iconPosition === "end"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
<span
class="wrn-btn__tooltip"
role="tooltip"
aria-hidden="true"
>
{ariaLabel || label}
</span>
{/if}
</button>
{/if}
</span>
}
style {
.wrn-spinner {
display: inline-block;
width: 1.15rem;
height: 1.15rem;
border: 2px solid var(--wrn-color-border);
border-top-color: var(--wrn-color-primary);
border-radius: 999px;
animation: wrn-spin 0.7s linear infinite;
}
.wrn-spinner--inline {
width: 1rem;
height: 1rem;
}
@keyframes wrn-spin {
to {
transform: rotate(360deg);
}
}
/* --- Button --------------------------------------------------------------- */
.wrn-btn {
position: relative;
--wrn-btn-color: var(--wrn-color-primary);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font: inherit;
font-size: 0.875rem;
font-weight: 600;
line-height: 1;
border: 1px solid transparent;
border-radius: var(--wrn-radius-sm);
padding: 0 0.75rem;
cursor: pointer;
text-decoration: none;
min-height: 2rem;
transition:
color var(--wrn-motion-base) var(--wrn-ease-standard),
border-color var(--wrn-motion-base) var(--wrn-ease-standard),
background var(--wrn-motion-base) var(--wrn-ease-standard),
box-shadow var(--wrn-motion-base) var(--wrn-ease-standard),
transform var(--wrn-motion-base) var(--wrn-ease-emphasized);
}
.wrn-btn:active {
transform: scale(0.98);
}
.wrn-btn:focus-visible {
outline: 2px solid var(--wrn-color-primary);
outline-offset: 2px;
}
.wrn-btn--color-primary {
--wrn-btn-color: var(--wrn-color-primary);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--color-secondary {
--wrn-btn-color: var(--wrn-color-muted);
--wrn-btn-contrast: var(--wrn-color-bg);
}
.wrn-btn--color-success {
--wrn-btn-color: var(--wrn-color-success);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--color-warning {
--wrn-btn-color: var(--wrn-color-warning);
--wrn-btn-contrast: var(--wrn-color-bg);
}
.wrn-btn--color-danger {
--wrn-btn-color: var(--wrn-color-danger);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--color-info {
--wrn-btn-color: var(--wrn-color-info);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--variant-default,
.wrn-btn--variant-solid,
.wrn-btn--default,
.wrn-btn--solid,
.wrn-btn--primary {
color: var(--wrn-btn-contrast);
background: var(--wrn-btn-color);
box-shadow:
0 1px 2px color-mix(in srgb, black 20%, transparent),
0 0 0 1px color-mix(in srgb, var(--wrn-btn-color) 72%, transparent);
}
.wrn-btn--variant-default:hover,
.wrn-btn--variant-solid:hover,
.wrn-btn--default:hover,
.wrn-btn--solid:hover,
.wrn-btn--primary:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 84%, black);
transform: translateY(-1px);
box-shadow:
0 0.35rem 0.9rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent),
0 0 0 1px color-mix(in srgb, var(--wrn-btn-color) 82%, transparent);
}
.wrn-btn--variant-outline,
.wrn-btn--outline {
color: var(--wrn-btn-color);
background: transparent;
border-color: color-mix(in srgb, var(--wrn-btn-color) 55%, var(--wrn-color-border));
}
.wrn-btn--variant-outline:hover,
.wrn-btn--outline:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 10%, transparent);
border-color: var(--wrn-btn-color);
}
.wrn-btn--variant-destructive,
.wrn-btn--danger,
.wrn-btn--destructive {
color: var(--wrn-btn-color);
background: color-mix(in srgb, var(--wrn-btn-color) 12%, transparent);
border-color: color-mix(in srgb, var(--wrn-btn-color) 28%, transparent);
}
.wrn-btn--danger,
.wrn-btn--destructive {
--wrn-btn-color: var(--wrn-color-danger);
}
.wrn-btn--variant-destructive:hover,
.wrn-btn--danger:hover,
.wrn-btn--destructive:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
}
.wrn-btn--variant-ghost,
.wrn-btn--ghost {
background: transparent;
color: var(--wrn-btn-color);
}
.wrn-btn--variant-ghost:hover,
.wrn-btn--ghost:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 10%, transparent);
}
.wrn-btn--variant-secondary,
.wrn-btn--secondary {
color: var(--wrn-color-text);
background: color-mix(in srgb, var(--wrn-btn-color) 10%, var(--wrn-color-surface));
border-color: color-mix(in srgb, var(--wrn-btn-color) 24%, var(--wrn-color-border));
box-shadow: var(--wrn-shadow-1);
}
.wrn-btn--variant-secondary:hover,
.wrn-btn--secondary:hover {
color: var(--wrn-btn-color);
border-color: color-mix(in srgb, var(--wrn-btn-color) 55%, var(--wrn-color-border));
background: color-mix(in srgb, var(--wrn-btn-color) 15%, var(--wrn-color-surface));
transform: translateY(-2px);
}
.wrn-btn--variant-link,
.wrn-btn--link {
min-height: auto;
padding: 0;
color: var(--wrn-btn-color);
background: transparent;
border-color: transparent;
border-radius: 0;
text-underline-offset: 0.22em;
}
.wrn-btn--variant-link:hover,
.wrn-btn--link:hover {
text-decoration: underline;
}
.wrn-btn:disabled,
.wrn-btn[aria-busy="true"] {
cursor: not-allowed;
opacity: 0.58;
transform: none;
box-shadow: none;
}
.wrn-btn--size-xs,
.wrn-btn--xs {
min-height: 1.5rem;
padding-inline: 0.5rem;
gap: 0.25rem;
font-size: 0.75rem;
}
.wrn-btn--size-sm,
.wrn-btn--sm {
min-height: 2.125rem;
padding-inline: 0.625rem;
gap: 0.25rem;
font-size: 0.8rem;
}
.wrn-btn--size-default,
.wrn-btn--md {
min-height: 2.5rem;
padding-inline: 0.75rem;
font-size: 0.875rem;
}
.wrn-btn--size-lg,
.wrn-btn--lg {
min-height: 2.75rem;
padding-inline: 0.875rem;
font-size: 0.925rem;
}
.wrn-btn--size-icon,
.wrn-btn--size-icon-xs,
.wrn-btn--size-icon-sm,
.wrn-btn--size-icon-lg,
.wrn-btn--icon,
.wrn-btn--icon-xs,
.wrn-btn--icon-sm,
.wrn-btn--icon-lg {
flex: none;
padding: 0;
}
.wrn-btn--size-icon,
.wrn-btn--icon {
width: 2rem;
height: 2rem;
}
.wrn-btn--size-icon-xs,
.wrn-btn--icon-xs {
width: 1.5rem;
min-height: 1.5rem;
}
.wrn-btn--size-icon-sm,
.wrn-btn--icon-sm {
width: 1.75rem;
min-height: 1.75rem;
}
.wrn-btn--size-icon-lg,
.wrn-btn--icon-lg {
width: 2.25rem;
min-height: 2.25rem;
}
.wrn-btn__icon {
width: 1rem;
height: 1rem;
flex: none;
}
.wrn-btn__label {
font-weight: 650;
}
.wrn-btn:hover > .wrn-btn__tooltip,
.wrn-btn:focus-visible > .wrn-btn__tooltip {
opacity: 1;
transform: translate(-50%, 0);
}
.wrn-action {
position: relative;
display: inline-flex;
}
.wrn-btn--pill {
border-radius: 999px;
}
.wrn-btn--with-description {
min-height: 3.5rem;
justify-content: flex-start;
padding: 0.6rem 0.875rem;
text-align: left;
}
.wrn-btn__copy {
display: grid;
min-width: 0;
gap: 0.15rem;
}
.wrn-btn__description {
color: currentColor;
font-size: 0.75rem;
font-weight: 450;
line-height: 1.35;
opacity: 0.74;
}
.wrn-btn__tooltip {
position: absolute;
z-index: 80;
inset-block-end: calc(100% + 0.5rem);
inset-inline-start: 50%;
width: max-content;
max-width: min(16rem, calc(100vw - 2rem));
padding: 0.4rem 0.6rem;
color: var(--wrn-color-bg);
background: var(--wrn-color-text);
border: 1px solid color-mix(in srgb, var(--wrn-color-text) 85%, var(--wrn-color-border));
border-radius: var(--wrn-radius-sm);
box-shadow: var(--wrn-shadow-2);
font-size: 0.75rem;
font-weight: 600;
line-height: 1.25;
text-align: center;
pointer-events: none;
opacity: 0;
transform: translate(-50%, 0.25rem);
transition:
opacity var(--wrn-motion-fast) var(--wrn-ease-standard),
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
}
}
-742
View File
@@ -1,742 +0,0 @@
component Card {
outputs {
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
navigate(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
title: string = "Card title"
subtitle: string = ""
description: string = ""
header: string = ""
footer: string = ""
imageSrc: string = ""
imageAlt: string = ""
imagePosition: string = "top"
actionLabel: string = ""
actionHref: string = ""
headerActions: unknown[] = []
navigation: unknown[] = []
activeNav: string = ""
mobileNavigation: boolean = false
alertTitle: string = ""
alertDescription: string = ""
empty: boolean = false
emptyTitle: string = "No data to show"
emptyIcon: string = "icon-[lucide--inbox]"
items: unknown[] = []
size: string = "md"
color: string = "primary"
variant: string = "default"
layout: string = "vertical"
align: string = "left"
hover: string = "none"
scrollable: boolean = false
maxHeight: string = "18rem"
dismissible: boolean = false
ariaLabel: string = ""
class: string = ""
}
state dismissed: boolean = false
functions {
// Outputs must go through output.*; a CustomEvent dispatched on the root
// bubbles past every parent @binding without being seen, because the
// runtime keeps parent handlers in a registry only the output proxy reads.
client function dispatchCardNavigation(sourceEvent, item, index) {
output.navigate({ component: "Card", item: item, index: index })
}
client function dispatchCardNavigationValue(sourceEvent) {
output.navigate({
component: "Card",
value: sourceEvent.currentTarget.value
})
}
client function dispatchCardHeaderAction(sourceEvent, action, index) {
output.action({ component: "Card", action: action, index: index })
}
client function dismissCard() {
dismissed = true
output.dismiss({ component: "Card", title: title })
}
}
view {
<div
{...attrs}
data-ui-component="Card"
data-wrn-card
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
class='wrn-component wrn-next--card wrn-component--color-{color} wrn-component--size-{size} wrn-next--variant-{variant} {class}'
>
{#if items.length > 0}
<div class="wrn-next__card-group">
{#each items as item, itemIndex}
<article
class="wrn-next__card-panel"
aria-label='{item.ariaLabel || item.title || item.label || "Card item"}'
data-index='{itemIndex}'
>
{#if item.imageSrc || item.image}
<div class="wrn-next__card-media">
<img
src='{item.imageSrc || item.image}'
alt='{item.imageAlt || item.alt || ""}'
loading='{item.loading || "lazy"}'
decoding="async"
@load='output.load({ item: item, index: itemIndex })'
@error='output.error({ item: item, index: itemIndex })'
/>
</div>
{/if}
<div class="wrn-next__card-content">
{#if item.title || item.label}
<h3>{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wrn-next__card-subtitle">{item.subtitle}</p>
{/if}
{#if item.description}
<p class="wrn-next__card-description">{item.description}</p>
{/if}
{#if item.actionLabel || item.href}
<a
href='{item.actionHref || item.href || "#"}'
class="wrn-next__card-action"
@click='output.action({ item: item, index: itemIndex })'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</div>
</article>
{/each}
</div>
{:else}
<article
class="wrn-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if imageSrc && imagePosition === "overlay"}
<div class="wrn-next__card-overlay">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='output.load({ src: imageSrc })'
@error='output.error({ src: imageSrc })'
/>
<div class="wrn-next__card-overlay-shade" aria-hidden="true"></div>
</div>
{/if}
{#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
<div class="wrn-next__card-media wrn-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='output.load({ src: imageSrc })'
@error='output.error({ src: imageSrc })'
/>
</div>
{/if}
<div class="wrn-next__card-content">
{#if header || title || subtitle || headerActions.length > 0 || dismissible}
<header class="wrn-next__card-header">
<div class="wrn-next__card-heading">
{#if header}
<p class="wrn-next__card-eyebrow">{header}</p>
{/if}
{#if title}
<h3>{title}</h3>
{/if}
{#if subtitle}
<p class="wrn-next__card-subtitle">{subtitle}</p>
{/if}
</div>
{#if headerActions.length > 0 || dismissible}
<div class="wrn-next__card-header-actions">
{#each headerActions as headerAction, actionIndex}
<button
type="button"
aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
@click='dispatchCardHeaderAction(event, headerAction, actionIndex)'
>
{#if headerAction.icon}
<span class='{headerAction.icon}' aria-hidden="true"></span>
{/if}
{#if headerAction.label && !headerAction.icon}
<span>{headerAction.label}</span>
{/if}
</button>
{/each}
{#if dismissible}
<button
type="button"
aria-label="Dismiss card"
@click='dismissCard(event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</div>
{/if}
</header>
{/if}
{#if navigation.length > 0}
<nav aria-label='{ariaLabel || title || "Card navigation"}' class="wrn-next__card-navigation">
{#if mobileNavigation}
<select
class="wrn-next__card-navigation-select"
aria-label='{ariaLabel || title || "Card navigation"}'
@change='dispatchCardNavigationValue(event)'
>
{#each navigation as navItem}
<option
value='{navItem.value || navItem.href || navItem.label}'
selected='{activeNav === navItem.value || activeNav === navItem.href}'
>
{navItem.label}
</option>
{/each}
</select>
{/if}
<div class="wrn-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
{#each navigation as navItem, navIndex}
<button
type="button"
aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
@click='dispatchCardNavigation(event, navItem, navIndex)'
>
{#if navItem.icon}
<span class='{navItem.icon}' aria-hidden="true"></span>
{/if}
<span>{navItem.label}</span>
{#if navItem.badge}
<span class="wrn-next__card-tab-badge">{navItem.badge}</span>
{/if}
</button>
{/each}
</div>
</nav>
{/if}
{#if alertTitle || alertDescription}
<div class="wrn-next__card-alert" role="status">
{#if alertTitle}
<strong>{alertTitle}</strong>
{/if}
{#if alertDescription}
<p>{alertDescription}</p>
{/if}
</div>
{/if}
<div
class="wrn-next__card-body"
data-scrollable='{scrollable}'
style='max-height: {scrollable ? maxHeight : "none"};'
>
{#if empty}
<div class="wrn-next__card-empty">
<span class='{emptyIcon}' aria-hidden="true"></span>
<p>{emptyTitle}</p>
</div>
{:else}
{#if description}
<p class="wrn-next__card-description">{description}</p>
{/if}
<slot></slot>
{/if}
</div>
{#if footer || actionLabel}
<footer class="wrn-next__card-footer">
{#if footer}
<span>{footer}</span>
{/if}
{#if actionLabel}
<a
href='{actionHref || "#"}'
class="wrn-next__card-action"
@click='output.action({ href: actionHref, label: actionLabel })'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</footer>
{/if}
</div>
{#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
<div class="wrn-next__card-media wrn-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='output.load({ src: imageSrc })'
@error='output.error({ src: imageSrc })'
/>
</div>
{/if}
</article>
{/if}
</div>
}
style {
.wrn-next__card-media--overlay {
aspect-ratio: 16 / 9;
min-height: 18rem;
}
.wrn-next__card-media--overlay > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
@media (max-width: 480px) {
.wrn-next__card-media--overlay {
aspect-ratio: 4 / 3;
min-height: 16rem;
}
}
.wrn-next--card {
--wrn-card-color: var(--wrn-component-color, var(--wrn-color-primary));
--wrn-card-padding: 1.25rem;
display: block;
width: 100%;
color: var(--wrn-color-text);
}
.wrn-next--card[data-size="sm"] {
--wrn-card-padding: 0.85rem;
}
.wrn-next--card[data-size="lg"] {
--wrn-card-padding: 1.75rem;
}
.wrn-next__card-panel {
position: relative;
display: flex;
overflow: hidden;
flex-direction: column;
width: 100%;
min-width: 0;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
transition:
transform var(--wrn-motion-base) var(--wrn-ease-emphasized),
border-color var(--wrn-motion-base) ease,
box-shadow var(--wrn-motion-base) var(--wrn-ease-emphasized);
}
.wrn-next--card[data-variant="top-border"] > .wrn-next__card-panel,
.wrn-next--card[data-variant="accent"] > .wrn-next__card-panel {
border-top: 4px solid var(--wrn-card-color);
}
.wrn-next--card[data-hover="raise"] > .wrn-next__card-panel:hover {
border-color: color-mix(in srgb, var(--wrn-card-color) 38%, var(--wrn-color-border));
box-shadow: var(--wrn-shadow-3);
transform: translateY(-4px);
}
.wrn-next__card-header,
.wrn-next__card-footer {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 0.85rem var(--wrn-card-padding);
background: var(--wrn-color-surface-2);
color: var(--wrn-color-muted);
font-size: 0.875rem;
}
.wrn-next__card-header {
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next__card-footer {
margin-top: auto;
border-top: 1px solid var(--wrn-color-border);
}
.wrn-next__card-header-actions {
display: inline-flex;
gap: 0.2rem;
align-items: center;
}
.wrn-next__card-header-actions button {
display: inline-grid;
width: 2rem;
height: 2rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: var(--wrn-color-muted);
cursor: pointer;
place-items: center;
}
.wrn-next__card-header-actions button:hover {
background: color-mix(in srgb, var(--wrn-card-color) 12%, transparent);
color: var(--wrn-card-color);
}
.wrn-next__card-header-actions button:focus-visible {
outline: 2px solid var(--wrn-card-color);
outline-offset: 1px;
}
.wrn-next__card-media {
position: relative;
overflow: hidden;
min-height: 10rem;
background: var(--wrn-color-surface-2);
}
.wrn-next__card-media > img {
display: block;
width: 100%;
height: 100%;
min-height: inherit;
object-fit: cover;
transition: transform 420ms var(--wrn-ease-emphasized);
}
.wrn-next--card[data-hover="image"] .wrn-next__card-panel:hover .wrn-next__card-media > img {
transform: scale(1.06);
}
.wrn-next__card-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
gap: 0.55rem;
padding: clamp(1.25rem, 4vw, 2rem);
background: linear-gradient(
180deg,
rgb(2 6 23 / 0.9) 0%,
rgb(2 6 23 / 0.68) 38%,
rgb(2 6 23 / 0.34) 58%,
rgb(2 6 23 / 0.86) 100%
);
color: white;
text-shadow: 0 1px 2px rgb(0 0 0 / 0.42);
}
.wrn-next__card-overlay h3 {
color: white;
font-size: clamp(1.1rem, 2.2vw, 1.35rem);
}
.wrn-next__card-overlay p {
max-width: 48rem;
color: #fff;
font-weight: 500;
line-height: 1.6;
text-shadow:
0 1px 2px rgb(0 0 0 / 0.9),
0 2px 8px rgb(0 0 0 / 0.62);
}
.wrn-next__card-overlay .wrn-next__card-subtitle {
color: rgb(255 255 255 / 0.78);
}
.wrn-next__card-overlay small {
margin-top: auto;
color: rgb(255 255 255 / 0.88);
font-weight: 600;
text-shadow: 0 1px 2px rgb(0 0 0 / 0.55);
}
.wrn-next__card-body {
display: grid;
gap: 0.65rem;
min-width: 0;
padding: var(--wrn-card-padding);
}
.wrn-next__card-body[data-scrollable="true"] {
max-height: var(--wrn-card-max-height);
overflow: auto;
overscroll-behavior: contain;
scrollbar-color: var(--wrn-card-color) transparent;
scrollbar-width: thin;
}
.wrn-next__card-body h3,
.wrn-next__card-body p,
.wrn-next__card-overlay h3,
.wrn-next__card-overlay p {
margin: 0;
}
.wrn-next__card-body p {
color: var(--wrn-color-muted);
line-height: 1.65;
}
.wrn-next__card-subtitle {
color: var(--wrn-color-muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.wrn-next__card-action {
display: inline-flex;
gap: 0.25rem;
align-items: center;
width: fit-content;
margin-top: 0.2rem;
color: var(--wrn-card-color);
font-weight: 700;
text-decoration: none;
}
.wrn-next__card-action:hover {
text-decoration: underline;
text-underline-offset: 0.2em;
}
.wrn-next__card-navigation {
background: var(--wrn-color-surface-2);
}
.wrn-next__card-tabs {
display: flex;
overflow-x: auto;
}
.wrn-next__card-tabs button {
min-height: 3rem;
flex: 1 0 auto;
padding-inline: 1rem;
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--wrn-color-muted);
cursor: pointer;
font-weight: 700;
}
.wrn-next__card-tabs button[data-active="true"] {
border-color: var(--wrn-card-color);
color: var(--wrn-color-text);
}
.wrn-next__card-navigation-select {
display: none;
width: calc(100% - 2rem);
min-height: 2.75rem;
margin: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
}
.wrn-next__card-alert {
display: flex;
gap: 0.35rem;
padding: 0.8rem var(--wrn-card-padding);
background: color-mix(in srgb, var(--wrn-card-color) 14%, var(--wrn-color-surface-2));
color: var(--wrn-color-text);
font-size: 0.875rem;
}
.wrn-next__card-empty {
display: grid;
min-height: 12rem;
color: var(--wrn-color-muted);
place-content: center;
place-items: center;
}
.wrn-next__card-empty > span {
margin-bottom: 0.75rem;
font-size: 2.25rem;
}
.wrn-next--card[data-align="center"] .wrn-next__card-body {
text-align: center;
place-items: center;
}
.wrn-next--card[data-layout="horizontal"] > .wrn-next__card-panel {
display: grid;
grid-template-columns: minmax(12rem, 42%) minmax(0, 1fr);
}
.wrn-next--card[data-layout="horizontal"] .wrn-next__card-media {
grid-row: 1 / span 4;
min-height: 100%;
}
.wrn-next__card-group {
display: grid;
grid-template-columns: repeat(var(--wrn-card-columns, 3), minmax(0, 1fr));
}
.wrn-next__card-group > .wrn-next__card-panel {
border-radius: 0;
box-shadow: none;
}
.wrn-next__card-group > .wrn-next__card-panel:not(:first-child) {
margin-inline-start: -1px;
}
.wrn-next__card-group > .wrn-next__card-panel:first-child {
border-start-start-radius: var(--wrn-radius-md);
border-end-start-radius: var(--wrn-radius-md);
}
.wrn-next__card-group > .wrn-next__card-panel:last-child {
border-start-end-radius: var(--wrn-radius-md);
border-end-end-radius: var(--wrn-radius-md);
}
@media (max-width: 768px) {
.wrn-next--card[data-layout="horizontal"] > .wrn-next__card-panel {
display: flex;
}
.wrn-next--card[data-layout="horizontal"] .wrn-next__card-media {
min-height: 12rem;
}
.wrn-next__card-group {
grid-template-columns: 1fr;
}
.wrn-next__card-group > .wrn-next__card-panel:not(:first-child) {
margin-top: -1px;
margin-inline-start: 0;
}
.wrn-next__card-group > .wrn-next__card-panel {
border-radius: 0;
}
.wrn-next__card-group > .wrn-next__card-panel:first-child {
border-start-start-radius: var(--wrn-radius-md);
border-start-end-radius: var(--wrn-radius-md);
}
.wrn-next__card-group > .wrn-next__card-panel:last-child {
border-end-start-radius: var(--wrn-radius-md);
border-end-end-radius: var(--wrn-radius-md);
}
}
@media (max-width: 480px) {
.wrn-next__card-tabs:has(+ .wrn-next__card-navigation-select) {
display: none;
}
.wrn-next__card-navigation-select {
display: block;
}
}
/* 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;
}
}
}

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