first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+156
View File
@@ -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` (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-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.
+16
View File
@@ -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>
}
}
+11
View File
@@ -0,0 +1,11 @@
// Rounded user image.
component Avatar {
props {
src = ""
alt = ""
class = ""
}
view {
<img class="wire-avatar {class}" src="{src}" alt="{alt}">
}
}
+11
View File
@@ -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>
}
}
+14
View File
@@ -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>
}
}
+9
View File
@@ -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>
}
}
+14
View File
@@ -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>
}
}
+9
View File
@@ -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>
}
}
+13
View File
@@ -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>
}
}
+9
View File
@@ -0,0 +1,9 @@
// Horizontal rule using theme border color.
component Divider {
props {
class = ""
}
view {
<hr class="wire-divider {class}">
}
}
+21
View File
@@ -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>
}
}
+11
View File
@@ -0,0 +1,11 @@
// Responsive-ish grid: `cols` columns (16) with a gap (scale 08).
component Grid {
props {
cols = "2"
gap = "4"
class = ""
}
view {
<div class="wire-grid wire-cols-{cols} wire-gap-{gap} {class}"><slot></slot></div>
}
}
+12
View File
@@ -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>
}
}
+13
View File
@@ -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}">
}
}
+11
View File
@@ -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>
}
}
+15
View File
@@ -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>
}
}
+10
View File
@@ -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>
}
}
+11
View File
@@ -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>
}
}
+9
View File
@@ -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>
}
}
+9
View File
@@ -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>
}
}
+10
View File
@@ -0,0 +1,10 @@
// Vertical stack: lays children out in a column with a gap (scale 08).
component Stack {
props {
gap = "4"
class = ""
}
view {
<div class="wire-stack wire-gap-{gap} {class}"><slot></slot></div>
}
}
+15
View File
@@ -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>
}
}
+12
View File
@@ -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>
}
}
+11
View File
@@ -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>
}
}
+12
View File
@@ -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>
}
}
+11
View File
@@ -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>
}
}
+10
View File
@@ -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>
}
}
+14
View File
@@ -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:*"
}
}
+48
View File
@@ -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];
}
+10
View File
@@ -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-");
});
+525
View File
@@ -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);
}