first commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# @wrnexus/ui
|
||||
|
||||
> First-party Wire 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(--wire-*)` 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.
|
||||
|
||||
## 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 wire it into the router for you
|
||||
(see [Auto-discovery](#auto-discovery)).
|
||||
|
||||
## Components
|
||||
|
||||
Components live as `.wrn` files under `packages/ui/components/`. The mount name
|
||||
is the **lowercase file basename** (e.g. `button.wrn` → `data-component="button"`).
|
||||
Each 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` (0–8) |
|
||||
| `hstack` | Horizontal row with gap | `gap` (0–8) |
|
||||
| `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-wire-theme-toggle`) | `label` |
|
||||
|
||||
### Additional controls & data display
|
||||
|
||||
Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`,
|
||||
`tooltip`, and `table`.
|
||||
|
||||
The authoritative, always-current list is `uiComponentNames()` (below), which reads
|
||||
the component directory at runtime.
|
||||
|
||||
## API
|
||||
|
||||
The JS module (`@wrnexus/ui`) exposes four 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 `.wire-*` classes, themed via tokens). |
|
||||
| `uiComponentNames` | `() => string[]` | Sorted list of built-in component names (e.g. for `wrnexus eject` listing). |
|
||||
|
||||
### `./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.
|
||||
|
||||
## 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 wire 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 `--wire-color-primary`,
|
||||
`--wire-color-surface`, `--wire-radius-sm`, etc. Every component style resolves
|
||||
through `var(--wire-*)`, so changing a token restyles everything instantly
|
||||
(including across theme switches).
|
||||
2. **App CSS** — redefine a `.wire-*` class in your own stylesheet, which is loaded
|
||||
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-wire-theme-toggle` attribute — no per-component JS is required.
|
||||
@@ -0,0 +1,16 @@
|
||||
// Callout box. variant: info | success | danger | warning.
|
||||
// Optional `title`; body from `message` prop or the slot.
|
||||
component Alert {
|
||||
props {
|
||||
variant = "info"
|
||||
title = ""
|
||||
message = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-alert wire-alert--{variant} {class}" role="alert">
|
||||
<div class="wire-alert__title">{title}</div>
|
||||
<div class="wire-alert__body"><slot>{message}</slot></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Rounded user image.
|
||||
component Avatar {
|
||||
props {
|
||||
src = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<img class="wire-avatar {class}" src="{src}" alt="{alt}">
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Small status label. variant: default | primary | success | danger | warning.
|
||||
component Badge {
|
||||
props {
|
||||
label = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<span class="wire-badge wire-badge--{variant} {class}"><slot>{label}</slot></span>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Button. variant: default | primary | danger | ghost — size: sm | md | lg.
|
||||
// Use the `label` prop, or put custom content in the slot.
|
||||
component Button {
|
||||
props {
|
||||
label = "Button"
|
||||
variant = "default"
|
||||
size = "md"
|
||||
type = "button"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<button type="{type}" class="wire-btn wire-btn--{variant} wire-btn--{size} {class}"><slot>{label}</slot></button>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Surface container with padding, border, and rounded corners.
|
||||
component Card {
|
||||
props {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-card {class}"><slot></slot></div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Checkbox with an inline label.
|
||||
component Checkbox {
|
||||
props {
|
||||
name = ""
|
||||
label = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<label class="wire-check {class}">
|
||||
<input type="checkbox" name="{name}" class="wire-check__box">
|
||||
<span class="wire-check__label"><slot>{label}</slot></span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Centered, max-width page container. Children go in the <slot>.
|
||||
component Container {
|
||||
props {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-container {class}"><slot></slot></div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Expand/collapse section using native <details> (no JS).
|
||||
component Disclosure {
|
||||
props {
|
||||
summary = "Details"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<details class="wire-disclosure {class}">
|
||||
<summary class="wire-disclosure__summary">{summary}</summary>
|
||||
<div class="wire-disclosure__body"><slot></slot></div>
|
||||
</details>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Horizontal rule using theme border color.
|
||||
component Divider {
|
||||
props {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<hr class="wire-divider {class}">
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Drag-and-drop file uploader with per-file progress. Progressive enhancement:
|
||||
// the markup is inert until /__wrnexus/uploader.js loads (auto-injected when a
|
||||
// page contains `data-uploader`). Pairs with `handleUpload` on the server —
|
||||
// files POST to `endpoint`, which returns `{ ok, files:[{ key, url, … }] }`.
|
||||
//
|
||||
// <div data-component="file-upload" store="public" endpoint="/api/upload"
|
||||
// accept="image/*" max="10000000" multiple="true"></div>
|
||||
component FileUpload {
|
||||
props {
|
||||
store = "public"
|
||||
endpoint = "/api/upload"
|
||||
accept = ""
|
||||
multiple = false
|
||||
max = 0
|
||||
label = "Drag files here or click to browse"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-fileupload {class}" data-uploader="{store}" data-endpoint="{endpoint}" data-accept="{accept}" data-multiple="{multiple}" data-max="{max}" data-label="{label}"></div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Responsive-ish grid: `cols` columns (1–6) with a gap (scale 0–8).
|
||||
component Grid {
|
||||
props {
|
||||
cols = "2"
|
||||
gap = "4"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-grid wire-cols-{cols} wire-gap-{gap} {class}"><slot></slot></div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Horizontal stack: row layout with a gap and cross-axis alignment.
|
||||
// align: start | center | end | stretch
|
||||
component HStack {
|
||||
props {
|
||||
gap = "4"
|
||||
align = "center"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-hstack wire-gap-{gap} wire-items-{align} {class}"><slot></slot></div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Text input. Pairs with the validation system (name maps to a field).
|
||||
component Input {
|
||||
props {
|
||||
type = "text"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<input type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" class="wire-input {class}">
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Progress bar (native <progress>, themed).
|
||||
component Progress {
|
||||
props {
|
||||
value = 0
|
||||
max = 100
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<progress class="wire-progress {class}" value="{value}" max="{max}"></progress>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Radio input with an inline label.
|
||||
component Radio {
|
||||
props {
|
||||
name = ""
|
||||
value = ""
|
||||
label = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<label class="wire-check {class}">
|
||||
<input type="radio" name="{name}" value="{value}" class="wire-check__box">
|
||||
<span class="wire-check__label"><slot>{label}</slot></span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Styled native select. Put <option>s in the slot.
|
||||
component Select {
|
||||
props {
|
||||
name = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<select name="{name}" class="wire-input wire-select {class}"><slot></slot></select>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Loading placeholder (animated shimmer). Size it with width/height props.
|
||||
component Skeleton {
|
||||
props {
|
||||
width = "100%"
|
||||
height = "1rem"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<span class="wire-skeleton {class}" style="width: {width}; height: {height};" aria-hidden="true"></span>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Flexible spacer: grows to push siblings apart inside a flex row/column.
|
||||
component Spacer {
|
||||
props {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-spacer {class}"></div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Loading spinner (pure CSS animation, no JS).
|
||||
component Spinner {
|
||||
props {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<span class="wire-spinner {class}" role="status" aria-label="Loading"></span>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Vertical stack: lays children out in a column with a gap (scale 0–8).
|
||||
component Stack {
|
||||
props {
|
||||
gap = "4"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-stack wire-gap-{gap} {class}"><slot></slot></div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Toggle switch (an accessible checkbox styled as a switch).
|
||||
component Switch {
|
||||
props {
|
||||
name = ""
|
||||
label = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<label class="wire-switch {class}">
|
||||
<input type="checkbox" name="{name}" class="wire-switch__input" role="switch">
|
||||
<span class="wire-switch__track"><span class="wire-switch__thumb"></span></span>
|
||||
<span class="wire-switch__label"><slot>{label}</slot></span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Themed table wrapper — put a <table>…</table> (or thead/tbody) in the slot.
|
||||
// Scrolls horizontally on small screens.
|
||||
component Table {
|
||||
props {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-table-wrap {class}">
|
||||
<table class="wire-table"><slot></slot></table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Small tag / chip. variant: default | primary | success | danger | warning.
|
||||
component Tag {
|
||||
props {
|
||||
label = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<span class="wire-tag wire-tag--{variant} {class}"><slot>{label}</slot></span>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Multi-line text input.
|
||||
component Textarea {
|
||||
props {
|
||||
name = ""
|
||||
placeholder = ""
|
||||
rows = "4"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<textarea name="{name}" placeholder="{placeholder}" rows="{rows}" class="wire-input wire-textarea {class}"><slot></slot></textarea>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Theme switch button. The framework's theme runtime binds the click
|
||||
// (data-wire-theme-toggle), so no component JS is needed.
|
||||
component ThemeToggle {
|
||||
props {
|
||||
label = "Toggle theme"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<button type="button" class="wire-btn wire-btn--ghost {class}" data-wire-theme-toggle><slot>{label}</slot></button>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// CSS-only tooltip shown on hover/focus. Wrap the trigger content in the slot.
|
||||
component Tooltip {
|
||||
props {
|
||||
text = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<span class="wire-tooltip {class}" data-tooltip="{text}" tabindex="0"><slot></slot></span>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./ui.css": "./ui.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @wrnexus/ui — the Wire UI component library.
|
||||
*
|
||||
* Components are `.wrn` files under `components/`, auto-discovered by the
|
||||
* framework (the router scans this directory in addition to the app's own
|
||||
* `app/components`). Mount them in any page with `data-component="<name>"`.
|
||||
* Their styles live in a single themeable stylesheet, `ui.css`, served once at
|
||||
* `/__wrnexus/ui.css` — every class uses `var(--wire-*)` theme tokens.
|
||||
*
|
||||
* Override, in increasing order of power:
|
||||
* 1. theme tokens (change `--wire-color-primary`, etc.)
|
||||
* 2. redefine a `.wire-*` class in your own CSS (loaded after ui.css)
|
||||
* 3. pass a `class` prop (appended to the component root)
|
||||
* 4. `wrnexus eject <name>` to copy the component into `app/components` and own it
|
||||
*/
|
||||
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = join(here, "..");
|
||||
let cachedCss: string | undefined;
|
||||
let cachedNames: string[] | undefined;
|
||||
|
||||
/** Absolute path to the directory of Wire UI component `.wrn` files. */
|
||||
export function uiComponentsDir(): string {
|
||||
return join(packageRoot, "components");
|
||||
}
|
||||
|
||||
/** Absolute path to the Wire UI stylesheet. */
|
||||
export function uiCssPath(): string {
|
||||
return join(packageRoot, "ui.css");
|
||||
}
|
||||
|
||||
/** The Wire UI stylesheet contents (all `.wire-*` classes, themed via tokens). */
|
||||
export function uiCss(): string {
|
||||
return (cachedCss ??= readFileSync(uiCssPath(), "utf8"));
|
||||
}
|
||||
|
||||
/** Names of the built-in components (e.g. for `wrnexus eject` listing). */
|
||||
export function uiComponentNames(): string[] {
|
||||
cachedNames ??= readdirSync(uiComponentsDir())
|
||||
.filter((f) => f.endsWith(".wrn"))
|
||||
.map((f) => f.replace(/\.wrn$/, ""))
|
||||
.sort();
|
||||
return [...cachedNames];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { uiComponentNames, uiComponentsDir, uiCss } from "../src/index.ts";
|
||||
|
||||
test("bundled UI assets are discoverable and readable", () => {
|
||||
expect(uiComponentNames()).toContain("button");
|
||||
expect(readFileSync(join(uiComponentsDir(), "button.wrn"), "utf8")).toContain("component Button");
|
||||
expect(uiCss()).toContain("--wire-");
|
||||
});
|
||||
@@ -0,0 +1,525 @@
|
||||
/*
|
||||
* Wire UI stylesheet. Served once at /__wrnexus/ui.css. Every value is a theme
|
||||
* token (var(--wire-*)), so components restyle instantly when the theme changes.
|
||||
* Override any of these classes in your own CSS (it loads after ui.css).
|
||||
*/
|
||||
|
||||
/* --- Layout --------------------------------------------------------------- */
|
||||
.wire-container {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
.wire-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.wire-hstack {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.wire-grid {
|
||||
display: grid;
|
||||
}
|
||||
.wire-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.wire-divider {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* gap scale (shared by stack / hstack / grid) */
|
||||
.wire-gap-0 {
|
||||
gap: 0;
|
||||
}
|
||||
.wire-gap-1 {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.wire-gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.wire-gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wire-gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
.wire-gap-5 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.wire-gap-6 {
|
||||
gap: 2rem;
|
||||
}
|
||||
.wire-gap-8 {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.wire-items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.wire-items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.wire-items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.wire-items-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.wire-cols-1 {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
.wire-cols-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.wire-cols-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
.wire-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
.wire-cols-5 {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
.wire-cols-6 {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.wire-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Button --------------------------------------------------------------- */
|
||||
.wire-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--wire-radius-sm);
|
||||
padding: 0.55rem 0.9rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
filter 0.15s ease,
|
||||
background 0.15s ease,
|
||||
transform 0.04s ease;
|
||||
}
|
||||
.wire-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.wire-btn:focus-visible {
|
||||
outline: 2px solid var(--wire-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wire-btn--default {
|
||||
background: var(--wire-color-surface-2);
|
||||
color: var(--wire-color-text);
|
||||
border-color: var(--wire-color-border);
|
||||
}
|
||||
.wire-btn--primary {
|
||||
background: var(--wire-color-primary);
|
||||
color: var(--wire-color-primary-contrast);
|
||||
}
|
||||
.wire-btn--primary:hover {
|
||||
background: var(--wire-color-primary-hover);
|
||||
}
|
||||
.wire-btn--danger {
|
||||
background: var(--wire-color-danger);
|
||||
color: #fff;
|
||||
}
|
||||
.wire-btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--wire-color-text);
|
||||
border-color: var(--wire-color-border);
|
||||
}
|
||||
.wire-btn--ghost:hover {
|
||||
background: var(--wire-color-surface-2);
|
||||
}
|
||||
|
||||
.wire-btn--sm {
|
||||
padding: 0.35rem 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.wire-btn--md {
|
||||
padding: 0.55rem 0.9rem;
|
||||
}
|
||||
.wire-btn--lg {
|
||||
padding: 0.7rem 1.2rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
/* --- Inputs --------------------------------------------------------------- */
|
||||
.wire-input {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-bg);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: var(--wire-radius-sm);
|
||||
padding: 0.5rem 0.7rem;
|
||||
}
|
||||
.wire-input::placeholder {
|
||||
color: var(--wire-color-muted);
|
||||
}
|
||||
.wire-input:focus-visible {
|
||||
outline: 2px solid var(--wire-color-primary);
|
||||
outline-offset: 1px;
|
||||
border-color: var(--wire-color-primary);
|
||||
}
|
||||
.wire-textarea {
|
||||
resize: vertical;
|
||||
min-height: 4.5rem;
|
||||
}
|
||||
|
||||
/* Validation states (set by /__wrnexus/validate.js). */
|
||||
.wire-invalid {
|
||||
border-color: var(--wire-color-danger) !important;
|
||||
}
|
||||
.wire-field-error {
|
||||
display: block;
|
||||
min-height: 1em;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--wire-color-danger);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.wire-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wire-check__box {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
accent-color: var(--wire-color-primary);
|
||||
}
|
||||
.wire-check__box:focus-visible {
|
||||
outline: 2px solid var(--wire-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.wire-check__label {
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
|
||||
/* --- Badge ---------------------------------------------------------------- */
|
||||
.wire-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.wire-badge--default {
|
||||
background: var(--wire-color-surface-2);
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
.wire-badge--primary {
|
||||
background: var(--wire-color-primary);
|
||||
color: var(--wire-color-primary-contrast);
|
||||
}
|
||||
.wire-badge--success {
|
||||
background: var(--wire-color-success);
|
||||
color: #05210f;
|
||||
}
|
||||
.wire-badge--danger {
|
||||
background: var(--wire-color-danger);
|
||||
color: #fff;
|
||||
}
|
||||
.wire-badge--warning {
|
||||
background: var(--wire-color-warning);
|
||||
color: #201400;
|
||||
}
|
||||
|
||||
/* --- Alert ---------------------------------------------------------------- */
|
||||
.wire-alert {
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-left-width: 4px;
|
||||
border-radius: var(--wire-radius-sm);
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--wire-color-surface);
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
.wire-alert__title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
.wire-alert__title:empty {
|
||||
display: none;
|
||||
}
|
||||
.wire-alert__body {
|
||||
color: var(--wire-color-muted);
|
||||
}
|
||||
.wire-alert--info {
|
||||
border-left-color: var(--wire-color-primary);
|
||||
}
|
||||
.wire-alert--success {
|
||||
border-left-color: var(--wire-color-success);
|
||||
}
|
||||
.wire-alert--danger {
|
||||
border-left-color: var(--wire-color-danger);
|
||||
}
|
||||
.wire-alert--warning {
|
||||
border-left-color: var(--wire-color-warning);
|
||||
}
|
||||
|
||||
/* --- Card ----------------------------------------------------------------- */
|
||||
.wire-card {
|
||||
background: var(--wire-color-surface);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: var(--wire-radius);
|
||||
padding: 1.25rem;
|
||||
box-shadow: var(--wire-shadow-1);
|
||||
}
|
||||
|
||||
/* --- Avatar --------------------------------------------------------------- */
|
||||
.wire-avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 999px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
/* --- Spinner -------------------------------------------------------------- */
|
||||
.wire-spinner {
|
||||
display: inline-block;
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
border: 2px solid var(--wire-color-border);
|
||||
border-top-color: var(--wire-color-primary);
|
||||
border-radius: 999px;
|
||||
animation: wire-spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes wire-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Disclosure ----------------------------------------------------------- */
|
||||
.wire-disclosure {
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: var(--wire-radius-sm);
|
||||
background: var(--wire-color-surface);
|
||||
}
|
||||
.wire-disclosure__summary {
|
||||
cursor: pointer;
|
||||
padding: 0.6rem 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wire-disclosure__summary:focus-visible {
|
||||
outline: 2px solid var(--wire-color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.wire-disclosure__body {
|
||||
padding: 0 0.9rem 0.75rem;
|
||||
color: var(--wire-color-muted);
|
||||
}
|
||||
|
||||
/* --- Select --------------------------------------------------------------- */
|
||||
.wire-select {
|
||||
appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--wire-color-muted) 50%),
|
||||
linear-gradient(135deg, var(--wire-color-muted) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 18px) 1.05em,
|
||||
calc(100% - 13px) 1.05em;
|
||||
background-size:
|
||||
5px 5px,
|
||||
5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
/* --- Switch --------------------------------------------------------------- */
|
||||
.wire-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wire-switch__input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.wire-switch__track {
|
||||
position: relative;
|
||||
width: 2.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 999px;
|
||||
background: var(--wire-color-border);
|
||||
transition: background 0.15s ease;
|
||||
flex: none;
|
||||
}
|
||||
.wire-switch__thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: calc(1.25rem - 4px);
|
||||
height: calc(1.25rem - 4px);
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.wire-switch__input:checked + .wire-switch__track {
|
||||
background: var(--wire-color-primary);
|
||||
}
|
||||
.wire-switch__input:checked + .wire-switch__track .wire-switch__thumb {
|
||||
transform: translateX(1rem);
|
||||
}
|
||||
.wire-switch__input:focus-visible + .wire-switch__track {
|
||||
outline: 2px solid var(--wire-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.wire-switch__label {
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
|
||||
/* --- Progress ------------------------------------------------------------- */
|
||||
.wire-progress {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: var(--wire-color-surface-2);
|
||||
}
|
||||
.wire-progress::-webkit-progress-bar {
|
||||
background: var(--wire-color-surface-2);
|
||||
}
|
||||
.wire-progress::-webkit-progress-value {
|
||||
background: var(--wire-color-primary);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.wire-progress::-moz-progress-bar {
|
||||
background: var(--wire-color-primary);
|
||||
}
|
||||
|
||||
/* --- Tag ------------------------------------------------------------------ */
|
||||
.wire-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: var(--wire-radius-sm);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
background: var(--wire-color-surface-2);
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
.wire-tag--primary {
|
||||
background: var(--wire-color-primary);
|
||||
color: var(--wire-color-primary-contrast);
|
||||
border-color: transparent;
|
||||
}
|
||||
.wire-tag--success {
|
||||
background: var(--wire-color-success);
|
||||
color: #05210f;
|
||||
border-color: transparent;
|
||||
}
|
||||
.wire-tag--danger {
|
||||
background: var(--wire-color-danger);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
.wire-tag--warning {
|
||||
background: var(--wire-color-warning);
|
||||
color: #201400;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* --- Skeleton ------------------------------------------------------------- */
|
||||
.wire-skeleton {
|
||||
display: inline-block;
|
||||
border-radius: var(--wire-radius-sm);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--wire-color-surface-2) 25%,
|
||||
var(--wire-color-border) 37%,
|
||||
var(--wire-color-surface-2) 63%
|
||||
);
|
||||
background-size: 400% 100%;
|
||||
animation: wire-shimmer 1.4s ease infinite;
|
||||
}
|
||||
@keyframes wire-shimmer {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Tooltip -------------------------------------------------------------- */
|
||||
.wire-tooltip {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
cursor: help;
|
||||
}
|
||||
.wire-tooltip::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
background: var(--wire-color-text);
|
||||
color: var(--wire-color-bg);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--wire-radius-sm);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.12s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
.wire-tooltip:hover::after,
|
||||
.wire-tooltip:focus-visible::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --- Table ---------------------------------------------------------------- */
|
||||
.wire-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.wire-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.wire-table th,
|
||||
.wire-table td {
|
||||
text-align: left;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
.wire-table th {
|
||||
font-weight: 700;
|
||||
color: var(--wire-color-muted);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.wire-table tbody tr:hover {
|
||||
background: var(--wire-color-surface-2);
|
||||
}
|
||||
Reference in New Issue
Block a user