refactor(ui): migrate the layout components off Tailwind utilities
Quality / quality (ubuntu-latest) (push) Failing after 12m47s
Quality / quality (windows-latest) (push) Canceled after 0s

Container, Columns, Grid, Divider, Image, Link, Typography and Kbd were built
from utility classes and class: conditionals. That works only where Tailwind
is present, and every variant cost a dozen conditional lines -- Divider spent
eleven of them saying which token to paint the rule.

They now carry wire-* classes with a local style block, and variants are data
attributes the style block selects on. Divider went from eleven conditionals
to five rules, and Typography lost thirteen.

Behaviour is preserved rather than improved on. Container keeps columns and
gap even though a container is not really a grid, because applications depend
on them, and its columns default stays 2: the redesign contract test caught
that changing it would silently reflow every Container already published.

Additive only: Grid gains minItemWidth for an auto-fit track, Divider gains
dashed and dotted variants, Image gains fit, and Link gains underline.

Verified in a browser rather than by eye, since the pane cannot screenshot:
track counts match the declared columns at desktop and collapse correctly
below each breakpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 11:33:59 +05:30
co-authored by Claude Opus 5
parent b56cb8cba5
commit 8aa28205e0
20 changed files with 935 additions and 230 deletions
+8 -8
View File
@@ -23,10 +23,10 @@
"packages/ui/components/Clipboard.wrn": "bb8651c7e44517db827603d0c0e546b6891093d7e6589c0b6e45d91b6187591b",
"packages/ui/components/Collapse.wrn": "c537239831c6938f3c39be0ca3ae8ba928c2490e21cfe5853c10f7221a4678ca",
"packages/ui/components/ColorPicker.wrn": "6940b7ae48f1358008b8c3ae6dc7da31f96a1aefadeef18252568b2cda0b5cbf",
"packages/ui/components/Columns.wrn": "449a427c2f4dd846b5239b73efe5b8d15ad4f20fa28598802a5eb57c33332391",
"packages/ui/components/Columns.wrn": "e01496aa53025444cc51671efcce40d6d24d86775b2cbb1ee0def1862bb6315d",
"packages/ui/components/Combobox.wrn": "62a7d861b0e536ceb088cea3552c59acbafaea5184101e3651dc8f4976b99dbc",
"packages/ui/components/Confetti.wrn": "22d687beefd5047055f65e806b4e39f81e18887532ec18d439ac364a044b230a",
"packages/ui/components/Container.wrn": "be8fce140043eced8b78c8dd14ff85ace4ed2d91671251641ef83e0fe4da8f2a",
"packages/ui/components/Container.wrn": "4a0acbb896f4c11714e3925d912b10ef9aeedfa904b4ad8561aa2cbd9a0fdca0",
"packages/ui/components/ContextMenu.wrn": "2e011e9cb1c09a3a2344ed3fa29dcc74331cd39f838535d8aee4b249dceef0ce",
"packages/ui/components/CopyMarkup.wrn": "8d57a7d72e02c126f855a181fc36e1b969edb90f683dce6b6ffe546d49d2b29b",
"packages/ui/components/CustomScrollbar.wrn": "fedb2ab06cc2cf9499c130590945ca6971a3edfb60407b6658ef7756e4be9823",
@@ -34,7 +34,7 @@
"packages/ui/components/DataTable.wrn": "5a76e50e8559c724aaf01ec954958ac640c99758d44f1eef8e6eb1cb6dc4f862",
"packages/ui/components/DatePicker.wrn": "18b07e59c27c2720bf960e9c04ad70d9e422ddcf0817fede74e71899811bb387",
"packages/ui/components/DeviceFrame.wrn": "a881cf2cc1f859b43cc3c7a1e1ca0fb40cea7a9e530ab5bdcb985aa9592d11c0",
"packages/ui/components/Divider.wrn": "e377f8005249cf4f6c51dd4d04ff9c6a70a3f4e3a79d2c02a8ab601754c8553a",
"packages/ui/components/Divider.wrn": "f5cc449611022b3d3eedf982ae48f6b1a289db7855cc9b6ea0248dcda807ffb3",
"packages/ui/components/DragAndDrop.wrn": "9a403ce9ed20d36211d8916911871875c9c7d0952a7de1a21a433eaa0b3c3324",
"packages/ui/components/Drawer.wrn": "04a77a69edfad6793b5ab60981a719f11061984c087d0d61663566e322649fef",
"packages/ui/components/Dropdown.wrn": "c61b46ae6e54f6aacac9f4ed422f6c659deea9a637fd944645acd37bf270b01d",
@@ -45,17 +45,17 @@
"packages/ui/components/FileUpload.wrn": "3346d8978e134c3a1e6bc742201a3fbb0cffb89da40da6fcd856a2adbb971393",
"packages/ui/components/FileUploadProgress.wrn": "83bcc8558c70fbe0e0670642f65998abd222c235f1031be46bd7c65e6b5c5154",
"packages/ui/components/Footer.wrn": "a5dfaa5f9548b7fdd4b556c63428e0ea8d22e5e776a5714d76619999438cf618",
"packages/ui/components/Grid.wrn": "b5546b899c509c1f3e8f5edad801bc59acf9fe3008a627f0e4476cd61b5b672e",
"packages/ui/components/Grid.wrn": "d089d8657298b4d33993ba4bf7b581f816c65db513d8e48cf1fdd18da19e05a8",
"packages/ui/components/Hero.wrn": "e4e39dcd59c273192c7e4136f6583279e20b0956bcba4cfbc3f6e01a1fade152",
"packages/ui/components/HeroActions.wrn": "c5720efe2ce7f6c114eb2ea4ccc3e8102a1099078ba23fd13770eddeb6aec640",
"packages/ui/components/Image.wrn": "4294358e2eddf06ae4384217dbff7693b609c16bf6549f54d6ad27df74d7ae27",
"packages/ui/components/Image.wrn": "8113441bf90ab0d1d8618d477125ad339021f39e853db89d656c72c492e7a20b",
"packages/ui/components/Input.wrn": "9ad394eed60f1b05bbd2ccd50dc2de7b78245c52c87afd46055d772c38b7b0c9",
"packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123",
"packages/ui/components/InputNumber.wrn": "a0aa4f4566bf54ffbef54c99648eab05b5f9870be6ea07639a4ecf3930afb631",
"packages/ui/components/Kbd.wrn": "d51fa614b36f971f885410785b481c2e6900b2416e6e7c8868dfda1ebd5f3b4c",
"packages/ui/components/Kbd.wrn": "5afaabbb15902bd8fb82cb4155cf6cbfff55600927b54d564d27e2dd8b752386",
"packages/ui/components/LayoutSplitter.wrn": "a86dc54b74aed6b05e770cd1802960d6309bba42fe80e899e8aaa8b1b7aa88e3",
"packages/ui/components/LegendIndicator.wrn": "6a7607b27a17196f899132eb952203a1073e4077365a94935f485e700cbac665",
"packages/ui/components/Link.wrn": "935ac24e8ebd067860dc9709ea9121710315ad6e02ce95cd5e982ae4d5978a02",
"packages/ui/components/Link.wrn": "b986abcc66bc0b1c6296f3ff4e560812e35d89cc680d5251d7a3883e68c1d053",
"packages/ui/components/List.wrn": "02f960a8e82fa049aac477bac9c1d9371394c86d9c47513138f61bffc79dc7d8",
"packages/ui/components/ListGroup.wrn": "37e2ec61ceab20ed020a834da03208c493702b41abcf586816e29f2e50d8ee11",
"packages/ui/components/Map.wrn": "7b8a0d5412a464fd7cab8977d816542c506d8c5dab8230a3c984c2caee407c91",
@@ -101,7 +101,7 @@
"packages/ui/components/TogglePassword.wrn": "405a85cbfa3d0ff0185b52d2805fba88497a5f25501f01b2438ed3a28591a38d",
"packages/ui/components/Tooltip.wrn": "e6c8c14a75062d04a2e64245df1a40e67785240573c73bae4b0b32e76ef822bc",
"packages/ui/components/TreeView.wrn": "1791a10135595b17b5c746af9e0476e8520d516ba46d64c79dc4efe0af2b500d",
"packages/ui/components/Typography.wrn": "a667e800e11d23da74b00047bd9c560a65a5920e5394f58f66a6e0b32572ce8e",
"packages/ui/components/Typography.wrn": "1b4d1b91dee0ff522d27c566becc3bc49bd9ec979ae6334e6a15fca5d88eccac",
"packages/ui/components/WysiwygEditor.wrn": "636e60b9f9be7a5807cca7ad20e0b0f370ee1ec6c1ecba5d0a577726cc86bc98",
"packages/ui/components/alert.wrn": "a6020b3fbf02481f76cea4cfa563a7165c7ccc2920492f6f6b79e98cbf2cf0fa",
"packages/ui/components/avatar.wrn": "571b70790aff28a5b7d6adbf0196e77ece1ce153a95b384148cb2bb5f74ab2e9",
@@ -6,6 +6,7 @@ page ContainerDetail {
state playground_columns = Number(ctx.url.searchParams.get("pg_columns") ?? "2")
state playground_gap = ctx.url.searchParams.get("pg_gap") ?? "md"
state playground_maxWidth = ctx.url.searchParams.get("pg_maxWidth") ?? "xl"
state playground_centered = (ctx.url.searchParams.get("pg_centered") ?? "true") === "true"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
title = "Container"
@@ -20,7 +21,7 @@ page ContainerDetail {
<span class="showcase-eyebrow">Layout component</span>
<h1>Container</h1>
<p>Constrain and align page content with responsive gutters and compact, wide, or full width options.</p>
<div class="detail-badges"><span>6 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>7 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
@@ -29,7 +30,7 @@ page ContainerDetail {
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Container size='{playground_size}' color='{playground_color}' columns='{playground_columns}' gap='{playground_gap}' maxWidth='{playground_maxWidth}' class='{playground_class}'><div class="showcase-content-block">Centered container content</div></Container></div>
<div class="playground-preview-source" data-playground-preview><Container size='{playground_size}' color='{playground_color}' columns='{playground_columns}' gap='{playground_gap}' maxWidth='{playground_maxWidth}' centered='{playground_centered}' class='{playground_class}'><div class="showcase-content-block">Centered container content</div></Container></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Container&gt;
@@ -40,8 +41,8 @@ page ContainerDetail {
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>6 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-container-size"><span><strong>size</strong><small>string</small></span><select id="playground-container-size" name="pg_size"><option value="default" selected>default</option><option value="compact">compact</option><option value="spacious">spacious</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-container-color"><span><strong>color</strong><small>string</small></span><select id="playground-container-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-container-columns"><span><strong>columns</strong><small>number</small></span><input id="playground-container-columns" name="pg_columns" type="number" value="2" /></label><label class="playground-field" for="playground-container-gap"><span><strong>gap</strong><small>string</small></span><select id="playground-container-gap" name="pg_gap"><option value="md" selected>md</option><option value="none">none</option><option value="xs">xs</option><option value="sm">sm</option><option value="lg">lg</option><option value="xl">xl</option><option value="2xl">2xl</option></select></label><label class="playground-field" for="playground-container-maxWidth"><span><strong>max Width</strong><small>string</small></span><select id="playground-container-maxWidth" name="pg_maxWidth"><option value="xl" selected>xl</option><option value="compact">compact</option><option value="lg">lg</option><option value="wide">wide</option><option value="2xl">2xl</option><option value="full">full</option></select></label><label class="playground-field" for="playground-container-class"><span><strong>class</strong><small>string</small></span><input id="playground-container-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
<header><div><span>Component props</span><strong>7 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-container-size"><span><strong>size</strong><small>string</small></span><select id="playground-container-size" name="pg_size"><option value="default" selected>default</option><option value="compact">compact</option><option value="spacious">spacious</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-container-color"><span><strong>color</strong><small>string</small></span><select id="playground-container-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-container-columns"><span><strong>columns</strong><small>number</small></span><input id="playground-container-columns" name="pg_columns" type="number" value="2" /></label><label class="playground-field" for="playground-container-gap"><span><strong>gap</strong><small>string</small></span><select id="playground-container-gap" name="pg_gap"><option value="md" selected>md</option><option value="none">none</option><option value="xs">xs</option><option value="sm">sm</option><option value="lg">lg</option><option value="xl">xl</option><option value="2xl">2xl</option></select></label><label class="playground-field" for="playground-container-maxWidth"><span><strong>max Width</strong><small>string</small></span><select id="playground-container-maxWidth" name="pg_maxWidth"><option value="xl" selected>xl</option><option value="compact">compact</option><option value="lg">lg</option><option value="wide">wide</option><option value="2xl">2xl</option><option value="full">full</option></select></label><label class="playground-toggle" for="playground-container-centered"><span><strong>centered</strong><small>boolean</small></span><input id="playground-container-centered" name="pg_centered" type="checkbox" value="true" checked data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-container-class"><span><strong>class</strong><small>string</small></span><input id="playground-container-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -57,7 +58,7 @@ page ContainerDetail {
<div id="container-demo-1" class="demo-canvas demo-canvas--1">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Container size="default" columns="2" maxWidth="xl" class="showcase-instance showcase-instance--1"><div class="showcase-content-block">Centered container content</div></Container></div>
<div class="demo-render"><Container size="default" columns="2" maxWidth="xl" centered="true" class="showcase-instance showcase-instance--1"><div class="showcase-content-block">Centered container content</div></Container></div>
</div>
<section class="demo-code" aria-label="Standard content container usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -76,7 +77,7 @@ page ContainerDetail {
<div id="container-demo-2" class="demo-canvas demo-canvas--2">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Container size="compact" columns="2" maxWidth="compact" class="showcase-instance showcase-instance--2"><div class="showcase-content-block">Compact readable content</div></Container></div>
<div class="demo-render"><Container size="compact" columns="2" maxWidth="compact" centered="true" class="showcase-instance showcase-instance--2"><div class="showcase-content-block">Compact readable content</div></Container></div>
</div>
<section class="demo-code" aria-label="Compact reading container usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -97,7 +98,7 @@ page ContainerDetail {
<div id="container-demo-3" class="demo-canvas demo-canvas--3">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Container size="spacious" columns="2" maxWidth="full" class="showcase-instance showcase-instance--3"><div class="showcase-content-block">Full-width dashboard content</div></Container></div>
<div class="demo-render"><Container size="spacious" columns="2" maxWidth="full" centered="true" class="showcase-instance showcase-instance--3"><div class="showcase-content-block">Full-width dashboard content</div></Container></div>
</div>
<section class="demo-code" aria-label="Full-width layout container usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -110,7 +111,7 @@ page ContainerDetail {
</div>
</article></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>columns</code></td><td>number</td><td><code>2</code></td><td>No</td></tr><tr><td><code>gap</code></td><td>string</td><td><code>"md"</code></td><td>No</td></tr><tr><td><code>maxWidth</code></td><td>string</td><td><code>"xl"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>columns</code></td><td>number</td><td><code>2</code></td><td>No</td></tr><tr><td><code>gap</code></td><td>string</td><td><code>"md"</code></td><td>No</td></tr><tr><td><code>maxWidth</code></td><td>string</td><td><code>"xl"</code></td><td>No</td></tr><tr><td><code>centered</code></td><td>boolean</td><td><code>true</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#container-demo-1">Standard content container</a><a href="#container-demo-2">Compact reading container</a><a href="#container-demo-3">Full-width layout container</a><a href="#api">Props API</a></div>
@@ -5,6 +5,7 @@ page DividerDetail {
state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary"
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Divider"
state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal"
state playground_variant = ctx.url.searchParams.get("pg_variant") ?? "solid"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
title = "Divider"
@@ -19,7 +20,7 @@ page DividerDetail {
<span class="showcase-eyebrow">Layout component</span>
<h1>Divider</h1>
<p>Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.</p>
<div class="detail-badges"><span>5 props</span><span>0 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>6 props</span><span>0 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
@@ -28,7 +29,7 @@ page DividerDetail {
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Divider size='{playground_size}' color='{playground_color}' label='{playground_label}' orientation='{playground_orientation}' class='{playground_class}' /></div>
<div class="playground-preview-source" data-playground-preview><Divider size='{playground_size}' color='{playground_color}' label='{playground_label}' orientation='{playground_orientation}' variant='{playground_variant}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Divider
@@ -39,8 +40,8 @@ page DividerDetail {
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>5 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-divider-size"><span><strong>size</strong><small>string</small></span><select id="playground-divider-size" name="pg_size"><option value="default" selected>default</option><option value="sm">sm</option><option value="lg">lg</option><option value="xs">xs</option><option value="md">md</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-divider-color"><span><strong>color</strong><small>string</small></span><select id="playground-divider-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-divider-label"><span><strong>label</strong><small>string</small></span><input id="playground-divider-label" name="pg_label" type="text" value="Divider" /></label><label class="playground-field" for="playground-divider-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-divider-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-field" for="playground-divider-class"><span><strong>class</strong><small>string</small></span><input id="playground-divider-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
<header><div><span>Component props</span><strong>6 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-divider-size"><span><strong>size</strong><small>string</small></span><select id="playground-divider-size" name="pg_size"><option value="default" selected>default</option><option value="sm">sm</option><option value="lg">lg</option><option value="xs">xs</option><option value="md">md</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-divider-color"><span><strong>color</strong><small>string</small></span><select id="playground-divider-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-divider-label"><span><strong>label</strong><small>string</small></span><input id="playground-divider-label" name="pg_label" type="text" value="Divider" /></label><label class="playground-field" for="playground-divider-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-divider-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-field" for="playground-divider-variant"><span><strong>variant</strong><small>string</small></span><input id="playground-divider-variant" name="pg_variant" type="text" value="solid" /></label><label class="playground-field" for="playground-divider-class"><span><strong>class</strong><small>string</small></span><input id="playground-divider-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -56,7 +57,7 @@ page DividerDetail {
<div id="divider-demo-1" class="demo-canvas demo-canvas--1">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Divider size="default" label="Related information" orientation="horizontal" class="showcase-instance showcase-instance--1" /></div>
<div class="demo-render"><Divider size="default" label="Related information" orientation="horizontal" variant="solid" class="showcase-instance showcase-instance--1" /></div>
</div>
<section class="demo-code" aria-label="Horizontal section divider usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -75,12 +76,13 @@ page DividerDetail {
<div id="divider-demo-2" class="demo-canvas demo-canvas--2">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Divider size="sm" label="" orientation="horizontal" class="showcase-instance showcase-instance--2" /></div>
<div class="demo-render"><Divider size="sm" label="" orientation="horizontal" variant="secondary" class="showcase-instance showcase-instance--2" /></div>
</div>
<section class="demo-code" aria-label="Minimal unlabeled divider usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Divider
size="sm"
variant="secondary"
/&gt;</code></pre>
</section>
</div>
@@ -94,7 +96,7 @@ page DividerDetail {
<div id="divider-demo-3" class="demo-canvas demo-canvas--3">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Divider size="lg" color="info" label="Actions" orientation="vertical" class="showcase-instance showcase-instance--3" /></div>
<div class="demo-render"><Divider size="lg" color="info" label="Actions" orientation="vertical" variant="success" class="showcase-instance showcase-instance--3" /></div>
</div>
<section class="demo-code" aria-label="Vertical toolbar divider usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -103,12 +105,13 @@ page DividerDetail {
color="info"
label="Actions"
orientation="vertical"
variant="success"
/&gt;</code></pre>
</section>
</div>
</article></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>orientation</code></td><td>string</td><td><code>"horizontal"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>orientation</code></td><td>string</td><td><code>"horizontal"</code></td><td>No</td></tr><tr><td><code>variant</code></td><td>string</td><td><code>"solid"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#divider-demo-1">Horizontal section divider</a><a href="#divider-demo-2">Minimal unlabeled divider</a><a href="#divider-demo-3">Vertical toolbar divider</a><a href="#api">Props API</a></div>
@@ -6,6 +6,7 @@ page GridDetail {
state playground_columns = Number(ctx.url.searchParams.get("pg_columns") ?? "2")
state playground_gap = ctx.url.searchParams.get("pg_gap") ?? "md"
state playground_maxWidth = ctx.url.searchParams.get("pg_maxWidth") ?? "xl"
state playground_minItemWidth = ctx.url.searchParams.get("pg_minItemWidth") ?? ""
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
title = "Grid"
@@ -20,7 +21,7 @@ page GridDetail {
<span class="showcase-eyebrow">Layout component</span>
<h1>Grid</h1>
<p>Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.</p>
<div class="detail-badges"><span>6 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>7 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
@@ -29,7 +30,7 @@ page GridDetail {
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Grid size='{playground_size}' color='{playground_color}' columns='{playground_columns}' gap='{playground_gap}' maxWidth='{playground_maxWidth}' class='{playground_class}'><div class="showcase-grid-items"><span>One</span><span>Two</span><span>Three</span></div></Grid></div>
<div class="playground-preview-source" data-playground-preview><Grid size='{playground_size}' color='{playground_color}' columns='{playground_columns}' gap='{playground_gap}' maxWidth='{playground_maxWidth}' minItemWidth='{playground_minItemWidth}' class='{playground_class}'><div class="showcase-grid-items"><span>One</span><span>Two</span><span>Three</span></div></Grid></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Grid /&gt;</code></pre>
@@ -38,8 +39,8 @@ page GridDetail {
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>6 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-grid-size"><span><strong>size</strong><small>string</small></span><select id="playground-grid-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-grid-color"><span><strong>color</strong><small>string</small></span><select id="playground-grid-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-grid-columns"><span><strong>columns</strong><small>number</small></span><input id="playground-grid-columns" name="pg_columns" type="number" value="2" /></label><label class="playground-field" for="playground-grid-gap"><span><strong>gap</strong><small>string</small></span><select id="playground-grid-gap" name="pg_gap"><option value="md" selected>md</option><option value="none">none</option><option value="sm">sm</option><option value="lg">lg</option><option value="xl">xl</option><option value="xs">xs</option><option value="2xl">2xl</option></select></label><label class="playground-field" for="playground-grid-maxWidth"><span><strong>max Width</strong><small>string</small></span><select id="playground-grid-maxWidth" name="pg_maxWidth"><option value="xl" selected>xl</option><option value="compact">compact</option><option value="lg">lg</option><option value="wide">wide</option><option value="2xl">2xl</option><option value="full">full</option></select></label><label class="playground-field" for="playground-grid-class"><span><strong>class</strong><small>string</small></span><input id="playground-grid-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
<header><div><span>Component props</span><strong>7 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-grid-size"><span><strong>size</strong><small>string</small></span><select id="playground-grid-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-grid-color"><span><strong>color</strong><small>string</small></span><select id="playground-grid-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-grid-columns"><span><strong>columns</strong><small>number</small></span><input id="playground-grid-columns" name="pg_columns" type="number" value="2" /></label><label class="playground-field" for="playground-grid-gap"><span><strong>gap</strong><small>string</small></span><select id="playground-grid-gap" name="pg_gap"><option value="md" selected>md</option><option value="none">none</option><option value="sm">sm</option><option value="lg">lg</option><option value="xl">xl</option><option value="xs">xs</option><option value="2xl">2xl</option></select></label><label class="playground-field" for="playground-grid-maxWidth"><span><strong>max Width</strong><small>string</small></span><select id="playground-grid-maxWidth" name="pg_maxWidth"><option value="xl" selected>xl</option><option value="compact">compact</option><option value="lg">lg</option><option value="wide">wide</option><option value="2xl">2xl</option><option value="full">full</option></select></label><label class="playground-field" for="playground-grid-minItemWidth"><span><strong>min Item Width</strong><small>string</small></span><input id="playground-grid-minItemWidth" name="pg_minItemWidth" type="text" value="" /></label><label class="playground-field" for="playground-grid-class"><span><strong>class</strong><small>string</small></span><input id="playground-grid-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -112,7 +113,7 @@ page GridDetail {
</div>
</article></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>columns</code></td><td>number</td><td><code>2</code></td><td>No</td></tr><tr><td><code>gap</code></td><td>string</td><td><code>"md"</code></td><td>No</td></tr><tr><td><code>maxWidth</code></td><td>string</td><td><code>"xl"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>columns</code></td><td>number</td><td><code>2</code></td><td>No</td></tr><tr><td><code>gap</code></td><td>string</td><td><code>"md"</code></td><td>No</td></tr><tr><td><code>maxWidth</code></td><td>string</td><td><code>"xl"</code></td><td>No</td></tr><tr><td><code>minItemWidth</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#grid-demo-1">Responsive three-column grid</a><a href="#grid-demo-2">Dense four-column grid</a><a href="#grid-demo-3">Full-width two-column grid</a><a href="#api">Props API</a></div>
@@ -8,6 +8,7 @@ page ImageDetail {
state playground_width = ctx.url.searchParams.get("pg_width") ?? ""
state playground_height = ctx.url.searchParams.get("pg_height") ?? ""
state playground_loading = ctx.url.searchParams.get("pg_loading") ?? "lazy"
state playground_fit = ctx.url.searchParams.get("pg_fit") ?? "cover"
state playground_rounded = (ctx.url.searchParams.get("pg_rounded") ?? "false") === "true"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
@@ -23,7 +24,7 @@ page ImageDetail {
<span class="showcase-eyebrow">Layout component</span>
<h1>Image</h1>
<p>Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.</p>
<div class="detail-badges"><span>9 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>10 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
@@ -32,7 +33,7 @@ page ImageDetail {
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Image size='{playground_size}' color='{playground_color}' src='{playground_src}' alt='{playground_alt}' width='{playground_width}' height='{playground_height}' loading='{playground_loading}' rounded='{playground_rounded}' class='{playground_class}' /></div>
<div class="playground-preview-source" data-playground-preview><Image size='{playground_size}' color='{playground_color}' src='{playground_src}' alt='{playground_alt}' width='{playground_width}' height='{playground_height}' loading='{playground_loading}' fit='{playground_fit}' rounded='{playground_rounded}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Image /&gt;</code></pre>
@@ -41,8 +42,8 @@ page ImageDetail {
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>9 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-image-size"><span><strong>size</strong><small>string</small></span><select id="playground-image-size" name="pg_size"><option value="default" selected>default</option><option value="sm">sm</option><option value="lg">lg</option><option value="xs">xs</option><option value="md">md</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-image-color"><span><strong>color</strong><small>string</small></span><select id="playground-image-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-image-src"><span><strong>src</strong><small>string</small></span><input id="playground-image-src" name="pg_src" type="text" value="" /></label><label class="playground-field" for="playground-image-alt"><span><strong>alt</strong><small>string</small></span><input id="playground-image-alt" name="pg_alt" type="text" value="" /></label><label class="playground-field" for="playground-image-width"><span><strong>width</strong><small>string</small></span><select id="playground-image-width" name="pg_width"><option value="" selected>Auto / default</option><option value="default">default</option><option value="compact">compact</option><option value="wide">wide</option><option value="full">full</option></select></label><label class="playground-field" for="playground-image-height"><span><strong>height</strong><small>string</small></span><input id="playground-image-height" name="pg_height" type="text" value="" /></label><label class="playground-field" for="playground-image-loading"><span><strong>loading</strong><small>string</small></span><select id="playground-image-loading" name="pg_loading"><option value="lazy" selected>lazy</option><option value="eager">eager</option></select></label><label class="playground-toggle" for="playground-image-rounded"><span><strong>rounded</strong><small>boolean</small></span><input id="playground-image-rounded" name="pg_rounded" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-image-class"><span><strong>class</strong><small>string</small></span><input id="playground-image-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
<header><div><span>Component props</span><strong>10 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-image-size"><span><strong>size</strong><small>string</small></span><select id="playground-image-size" name="pg_size"><option value="default" selected>default</option><option value="sm">sm</option><option value="lg">lg</option><option value="xs">xs</option><option value="md">md</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-image-color"><span><strong>color</strong><small>string</small></span><select id="playground-image-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-image-src"><span><strong>src</strong><small>string</small></span><input id="playground-image-src" name="pg_src" type="text" value="" /></label><label class="playground-field" for="playground-image-alt"><span><strong>alt</strong><small>string</small></span><input id="playground-image-alt" name="pg_alt" type="text" value="" /></label><label class="playground-field" for="playground-image-width"><span><strong>width</strong><small>string</small></span><select id="playground-image-width" name="pg_width"><option value="" selected>Auto / default</option><option value="default">default</option><option value="compact">compact</option><option value="wide">wide</option><option value="full">full</option></select></label><label class="playground-field" for="playground-image-height"><span><strong>height</strong><small>string</small></span><input id="playground-image-height" name="pg_height" type="text" value="" /></label><label class="playground-field" for="playground-image-loading"><span><strong>loading</strong><small>string</small></span><select id="playground-image-loading" name="pg_loading"><option value="lazy" selected>lazy</option><option value="eager">eager</option></select></label><label class="playground-field" for="playground-image-fit"><span><strong>fit</strong><small>string</small></span><input id="playground-image-fit" name="pg_fit" type="text" value="cover" /></label><label class="playground-toggle" for="playground-image-rounded"><span><strong>rounded</strong><small>boolean</small></span><input id="playground-image-rounded" name="pg_rounded" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-image-class"><span><strong>class</strong><small>string</small></span><input id="playground-image-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -121,7 +122,7 @@ page ImageDetail {
</div>
</article></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>src</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>alt</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>width</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>height</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>loading</code></td><td>string</td><td><code>"lazy"</code></td><td>No</td></tr><tr><td><code>rounded</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>src</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>alt</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>width</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>height</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>loading</code></td><td>string</td><td><code>"lazy"</code></td><td>No</td></tr><tr><td><code>fit</code></td><td>string</td><td><code>"cover"</code></td><td>No</td></tr><tr><td><code>rounded</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#image-demo-1">Responsive content image</a><a href="#image-demo-2">Rounded thumbnail</a><a href="#image-demo-3">Eager hero image</a><a href="#api">Props API</a></div>
@@ -106,7 +106,7 @@ page KbdDetail {
</div>
</article></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"K"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"K"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#kbd-demo-1">Production default</a><a href="#kbd-demo-2">Compact application</a><a href="#kbd-demo-3">Rich configuration</a><a href="#api">Props API</a></div>
@@ -7,6 +7,7 @@ page LinkDetail {
state playground_href = ctx.url.searchParams.get("pg_href") ?? "#link-demo"
state playground_target = ctx.url.searchParams.get("pg_target") ?? ""
state playground_rel = ctx.url.searchParams.get("pg_rel") ?? ""
state playground_underline = ctx.url.searchParams.get("pg_underline") ?? "hover"
state playground_external = (ctx.url.searchParams.get("pg_external") ?? "false") === "true"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
@@ -22,7 +23,7 @@ page LinkDetail {
<span class="showcase-eyebrow">Layout component</span>
<h1>Link</h1>
<p>Render an accessible internal or external link with target, relation, size, color, and public focus or click events.</p>
<div class="detail-badges"><span>8 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>9 props</span><span>1 slots</span><span>0 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
@@ -31,7 +32,7 @@ page LinkDetail {
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Link size='{playground_size}' color='{playground_color}' label='{playground_label}' href='{playground_href}' target='{playground_target}' rel='{playground_rel}' external='{playground_external}' class='{playground_class}' /></div>
<div class="playground-preview-source" data-playground-preview><Link size='{playground_size}' color='{playground_color}' label='{playground_label}' href='{playground_href}' target='{playground_target}' rel='{playground_rel}' underline='{playground_underline}' external='{playground_external}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Link
@@ -42,8 +43,8 @@ page LinkDetail {
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>8 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-link-size"><span><strong>size</strong><small>string</small></span><select id="playground-link-size" name="pg_size"><option value="default" selected>default</option><option value="sm">sm</option><option value="lg">lg</option><option value="xs">xs</option><option value="md">md</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-link-color"><span><strong>color</strong><small>string</small></span><select id="playground-link-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-link-label"><span><strong>label</strong><small>string</small></span><input id="playground-link-label" name="pg_label" type="text" value="Link" /></label><label class="playground-field" for="playground-link-href"><span><strong>href</strong><small>string</small></span><input id="playground-link-href" name="pg_href" type="text" value="#link-demo" /></label><label class="playground-field" for="playground-link-target"><span><strong>target</strong><small>string</small></span><select id="playground-link-target" name="pg_target"><option value="" selected>Auto / default</option><option value="_self">_self</option><option value="_blank">_blank</option><option value="_parent">_parent</option><option value="_top">_top</option></select></label><label class="playground-field" for="playground-link-rel"><span><strong>rel</strong><small>string</small></span><input id="playground-link-rel" name="pg_rel" type="text" value="" /></label><label class="playground-toggle" for="playground-link-external"><span><strong>external</strong><small>boolean</small></span><input id="playground-link-external" name="pg_external" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-link-class"><span><strong>class</strong><small>string</small></span><input id="playground-link-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
<header><div><span>Component props</span><strong>9 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-link-size"><span><strong>size</strong><small>string</small></span><select id="playground-link-size" name="pg_size"><option value="default" selected>default</option><option value="sm">sm</option><option value="lg">lg</option><option value="xs">xs</option><option value="md">md</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-link-color"><span><strong>color</strong><small>string</small></span><select id="playground-link-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-link-label"><span><strong>label</strong><small>string</small></span><input id="playground-link-label" name="pg_label" type="text" value="Link" /></label><label class="playground-field" for="playground-link-href"><span><strong>href</strong><small>string</small></span><input id="playground-link-href" name="pg_href" type="text" value="#link-demo" /></label><label class="playground-field" for="playground-link-target"><span><strong>target</strong><small>string</small></span><select id="playground-link-target" name="pg_target"><option value="" selected>Auto / default</option><option value="_self">_self</option><option value="_blank">_blank</option><option value="_parent">_parent</option><option value="_top">_top</option></select></label><label class="playground-field" for="playground-link-rel"><span><strong>rel</strong><small>string</small></span><input id="playground-link-rel" name="pg_rel" type="text" value="" /></label><label class="playground-field" for="playground-link-underline"><span><strong>underline</strong><small>string</small></span><input id="playground-link-underline" name="pg_underline" type="text" value="hover" /></label><label class="playground-toggle" for="playground-link-external"><span><strong>external</strong><small>boolean</small></span><input id="playground-link-external" name="pg_external" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-link-class"><span><strong>class</strong><small>string</small></span><input id="playground-link-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -116,7 +117,7 @@ page LinkDetail {
</div>
</article></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Link"</code></td><td>No</td></tr><tr><td><code>href</code></td><td>string</td><td><code>"#"</code></td><td>No</td></tr><tr><td><code>target</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>rel</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>external</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Link"</code></td><td>No</td></tr><tr><td><code>href</code></td><td>string</td><td><code>"#"</code></td><td>No</td></tr><tr><td><code>target</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>rel</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>underline</code></td><td>string</td><td><code>"hover"</code></td><td>No</td></tr><tr><td><code>external</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#link-demo-1">Standard navigation link</a><a href="#link-demo-2">External resource link</a><a href="#link-demo-3">Large emphasized link</a><a href="#api">Props API</a></div>
@@ -26,11 +26,11 @@ page LayoutShowcase {
<article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><Container size="default" columns="2" maxWidth="xl" class="showcase-instance showcase-instance--1"><div class="showcase-content-block">Centered container content</div></Container></div>
<div class="catalog-card-stage"><Container size="default" columns="2" maxWidth="xl" centered="true" class="showcase-instance showcase-instance--1"><div class="showcase-content-block">Centered container content</div></Container></div>
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Layout</span><h2>Container</h2><p>Constrain and align page content with responsive gutters and compact, wide, or full width options.</p></div>
<div class="catalog-card-footer"><span>6 props · 1 slots</span><a href="/components/container">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>7 props · 1 slots</span><a href="/components/container">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -46,11 +46,11 @@ page LayoutShowcase {
<article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><Divider size="default" label="Related information" orientation="horizontal" class="showcase-instance showcase-instance--1" /></div>
<div class="catalog-card-stage"><Divider size="default" label="Related information" orientation="horizontal" variant="solid" class="showcase-instance showcase-instance--1" /></div>
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Layout</span><h2>Divider</h2><p>Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.</p></div>
<div class="catalog-card-footer"><span>5 props · 0 slots</span><a href="/components/divider">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>6 props · 0 slots</span><a href="/components/divider">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -80,7 +80,7 @@ page LayoutShowcase {
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Layout</span><h2>Grid</h2><p>Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.</p></div>
<div class="catalog-card-footer"><span>6 props · 1 slots</span><a href="/components/grid">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>7 props · 1 slots</span><a href="/components/grid">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -90,7 +90,7 @@ page LayoutShowcase {
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Layout</span><h2>Image</h2><p>Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.</p></div>
<div class="catalog-card-footer"><span>9 props · 1 slots</span><a href="/components/image">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>10 props · 1 slots</span><a href="/components/image">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -120,7 +120,7 @@ page LayoutShowcase {
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Layout</span><h2>Link</h2><p>Render an accessible internal or external link with target, relation, size, color, and public focus or click events.</p></div>
<div class="catalog-card-footer"><span>8 props · 1 slots</span><a href="/components/link">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>9 props · 1 slots</span><a href="/components/link">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -358,7 +358,7 @@
"category": "layout",
"purpose": "Constrain and align page content with responsive gutters and compact, wide, or full width options.",
"demoCount": 3,
"propCount": 6,
"propCount": 7,
"slots": ["default"],
"events": [],
"profiled": true
@@ -463,7 +463,7 @@
"category": "layout",
"purpose": "Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.",
"demoCount": 3,
"propCount": 5,
"propCount": 6,
"slots": [],
"events": [],
"profiled": true
@@ -595,7 +595,7 @@
"category": "layout",
"purpose": "Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.",
"demoCount": 3,
"propCount": 6,
"propCount": 7,
"slots": ["default"],
"events": [],
"profiled": true
@@ -631,7 +631,7 @@
"category": "layout",
"purpose": "Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.",
"demoCount": 3,
"propCount": 9,
"propCount": 10,
"slots": ["default"],
"events": [],
"profiled": true
@@ -715,7 +715,7 @@
"category": "layout",
"purpose": "Render an accessible internal or external link with target, relation, size, color, and public focus or click events.",
"demoCount": 3,
"propCount": 8,
"propCount": 9,
"slots": ["default"],
"events": [],
"profiled": true
+6 -6
View File
@@ -624,7 +624,7 @@ Create responsive balanced content columns with configurable count, gap, density
Constrain and align page content with responsive gutters and compact, wide, or full width options.
- Mount: `data-component="Container"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `centered: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -642,7 +642,7 @@ Theme-aware, responsive custom scrollbar component.
Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.
- Mount: `data-component="Divider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `variant: string = "solid"`, `class: string = ""`
- Slots: None
- Outputs: None
@@ -669,7 +669,7 @@ Render structured responsive footer navigation, pre and post content, copyright
Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.
- Mount: `data-component="Grid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `minItemWidth: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -678,7 +678,7 @@ Arrange arbitrary content in a responsive configurable CSS grid with stable colu
Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.
- Mount: `data-component="Image"`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `rounded: boolean = false`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `fit: string = "cover"`, `rounded: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -687,7 +687,7 @@ Render a responsive image with explicit dimensions, loading behavior, alternativ
Theme-aware, responsive kbd component.
- Mount: `data-component="Kbd"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -705,7 +705,7 @@ Theme-aware, responsive layout splitter component.
Render an accessible internal or external link with target, relation, size, color, and public focus or click events.
- Mount: `data-component="Link"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `underline: string = "hover"`, `external: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
+36 -1
View File
@@ -4153,6 +4153,13 @@
"default": "\"xl\"",
"options": []
},
{
"name": "centered",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "class",
"type": "string",
@@ -5194,6 +5201,13 @@
"default": "\"horizontal\"",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"solid\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -6550,6 +6564,13 @@
"default": "\"xl\"",
"options": []
},
{
"name": "minItemWidth",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -6964,6 +6985,13 @@
"default": "\"lazy\"",
"options": []
},
{
"name": "fit",
"type": "string",
"required": false,
"default": "\"cover\"",
"options": []
},
{
"name": "rounded",
"type": "boolean",
@@ -7752,7 +7780,7 @@
"name": "label",
"type": "string",
"required": false,
"default": "\"K\"",
"default": "\"K\"",
"options": []
},
{
@@ -7952,6 +7980,13 @@
"default": "\"\"",
"options": []
},
{
"name": "underline",
"type": "string",
"required": false,
"default": "\"hover\"",
"options": []
},
{
"name": "external",
"type": "boolean",
+88 -14
View File
@@ -1,3 +1,13 @@
// Columns -- a simple multi-column split for page content.
//
// <Columns columns={2} gap="lg">...</Columns>
//
// Columns and Grid overlap deliberately: Columns is the coarse two or three
// way split of a page, and its gaps are wider for that reason. Grid is the one
// to reach for when the items are cards and the count matters.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Columns {
props {
size: string = "default"
@@ -8,28 +18,92 @@ component Columns {
class: string = ""
}
functions {
shared function columnCount() {
var value = Number(columns)
if (!value || value < 1) {
return 1
}
return Math.min(4, value)
}
}
view {
<div
{...attrs}
data-ui-component="Columns"
class='wire-columns {class}'
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 items-start {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:md:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:gap-3='gap === "sm"'
class:gap-6='gap === "md"'
class:gap-10='gap === "lg"'
class:gap-14='gap === "xl"'
data-max-width='{maxWidth}'
data-columns='{columnCount()}'
>
<slot></slot>
</div>
}
style {
.wire-columns {
--columns-gap: 1.5rem;
display: grid;
width: 100%;
min-width: 0;
gap: var(--columns-gap);
grid-template-columns: 1fr;
align-items: start;
}
.wire-columns[data-max-width="md"] {
max-width: 48rem;
}
.wire-columns[data-max-width="lg"] {
max-width: 64rem;
}
.wire-columns[data-max-width="xl"] {
max-width: 80rem;
}
.wire-columns[data-max-width="2xl"] {
max-width: 96rem;
}
.wire-columns[data-max-width="full"] {
max-width: none;
}
.wire-columns[data-gap="sm"] {
--columns-gap: 0.75rem;
}
.wire-columns[data-gap="lg"] {
--columns-gap: 2.5rem;
}
.wire-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) {
.wire-columns:not([data-columns="1"]) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.wire-columns[data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wire-columns[data-columns="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
}
}
+92 -12
View File
@@ -1,3 +1,13 @@
// Image -- a framed image with a reserved aspect ratio.
//
// <Image src="/hero.jpg" alt="Dashboard" size="video" rounded={true} />
//
// The frame keeps its ratio whether or not the image has loaded, so the page
// does not jump when it arrives. Without a src it renders a labelled
// placeholder of the same shape rather than collapsing.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Image {
props {
size: string = "default"
@@ -7,41 +17,111 @@ component Image {
width: string = ""
height: string = ""
loading: string = "lazy"
fit: string = "cover"
rounded: boolean = false
class: string = ""
}
view {
<figure
{...attrs}
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='rounded'
class='wire-image {class}'
data-size='{size}'
data-color='{color}'
data-fit='{fit}'
data-rounded='{rounded}'
>
{#if src}
<img
class="wire-image__img"
data-show="src"
src='{src}'
alt='{alt}'
width='{width}'
height='{height}'
loading='{loading}'
decoding="async"
class="block h-auto w-full object-cover transition duration-300"
class:aspect-square='size === "square"'
class:aspect-video='size === "video"'
class:aspect-[4/3]='size === "landscape"'
class:aspect-[3/4]='size === "portrait"'
/>
{:else}
<div
class="flex min-h-48 w-full items-center justify-center text-[var(--wire-color-text-muted)]"
class="wire-image__placeholder"
data-show="!src"
role="img"
aria-label='{alt || "Image placeholder"}'
>
<span class="icon-[lucide--image] size-8" aria-hidden="true"></span>
<span class="icon-[lucide--image] wire-image__placeholder-icon" aria-hidden="true"></span>
</div>
{/if}
<slot></slot>
</figure>
}
style {
.wire-image {
position: relative;
margin: 0;
overflow: hidden;
width: 100%;
min-width: 0;
background: var(--wire-color-surface-soft);
}
.wire-image[data-rounded="true"] {
border-radius: var(--wire-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.
*/
.wire-image[data-size="square"] {
aspect-ratio: 1 / 1;
}
.wire-image[data-size="video"] {
aspect-ratio: 16 / 9;
}
.wire-image[data-size="landscape"] {
aspect-ratio: 4 / 3;
}
.wire-image[data-size="portrait"] {
aspect-ratio: 3 / 4;
}
.wire-image__img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.wire-image[data-fit="contain"] .wire-image__img {
object-fit: contain;
}
.wire-image[data-fit="fill"] .wire-image__img {
object-fit: fill;
}
.wire-image[data-size="default"] .wire-image__img {
height: auto;
}
.wire-image__placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 12rem;
height: 100%;
color: var(--wire-color-text-muted);
}
.wire-image__placeholder-icon {
width: 2rem;
height: 2rem;
}
}
}
+48 -2
View File
@@ -1,9 +1,55 @@
// Kbd -- a keyboard key.
//
// <Kbd label="Ctrl" />
//
// Deliberately small. A kbd element is a kbd element; the work here is making
// it look like a key and follow the theme.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Kbd {
props {
size: string = "default"
color: string = "primary"
label: string = "K"
label: string = "K"
class: string = ""
}
view { <kbd class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--kbd {class}"><slot>{label}</slot></kbd> }
view {
<kbd {...attrs} class='wire-kbd {class}' data-size='{size}' data-color='{color}'>
<slot>{label}</slot>
</kbd>
}
style {
.wire-kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.6rem;
padding: 0.15rem 0.4rem;
border: 1px solid var(--wire-color-border);
/* The lower edge is what reads as a physical key. */
border-bottom-width: 2px;
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface-soft);
color: var(--wire-color-text);
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
line-height: 1;
}
.wire-kbd[data-size="sm"] {
min-width: 1.4rem;
padding: 0.1rem 0.3rem;
font-size: 0.7rem;
}
.wire-kbd[data-size="lg"] {
min-width: 2rem;
padding: 0.3rem 0.55rem;
font-size: 0.9rem;
}
}
}
+111 -21
View File
@@ -1,3 +1,13 @@
// Link -- a text link that follows the theme.
//
// <Link href="/docs" label="Documentation" />
// <Link href="https://example.com" label="Spec" external={true} />
//
// external adds rel="noopener noreferrer" and an outbound icon, so the reader
// is told the link leaves the site before they follow it.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Link {
props {
size: string = "default"
@@ -6,42 +16,122 @@ component Link {
href: string = "#"
target: string = ""
rel: string = ""
underline: string = "hover"
external: boolean = false
class: string = ""
}
view {
<a
{...attrs}
data-ui-component="Link"
class='wire-link {class}'
href='{href}'
target='{target}'
rel='{external ? (rel || "noopener noreferrer") : rel}'
class='inline-flex min-w-0 items-center gap-1.5 rounded-md font-semibold underline-offset-4 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-background)] {class}'
class:text-xs='size === "xs"'
class:text-sm='size === "sm" || size === "default"'
class:text-base='size === "md"'
class:text-lg='size === "lg"'
class:text-[var(--wire-color-primary)]='color === "primary"'
class:hover:text-[var(--wire-color-primary-hover)]='color === "primary"'
class:text-[var(--wire-color-secondary)]='color === "secondary"'
class:hover:text-[var(--wire-color-secondary-hover)]='color === "secondary"'
class:text-[var(--wire-color-success)]='color === "success"'
class:text-[var(--wire-color-warning-text)]='color === "warning"'
class:text-[var(--wire-color-danger)]='color === "danger"'
class:text-[var(--wire-color-info)]='color === "info"'
class:text-[var(--wire-color-text)]='color === "neutral"'
class:hover:underline='external === false'
data-size='{size}'
data-color='{color}'
data-underline='{underline}'
data-external='{external}'
>
<span class="truncate">{label}</span>
{#if external}
<span class="wire-link__label">{label}</span>
<span
class="icon-[lucide--external-link] size-3.5 shrink-0"
class="icon-[lucide--external-link] wire-link__icon"
data-show="external"
aria-hidden="true"
></span>
{/if}
<slot></slot>
</a>
}
style {
.wire-link {
--link-tone: var(--wire-color-primary);
--link-tone-hover: var(--wire-color-primary-hover);
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-width: 0;
border-radius: var(--wire-radius-sm);
color: var(--link-tone);
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
text-underline-offset: 4px;
transition: color 140ms ease;
}
.wire-link[data-color="secondary"] {
--link-tone: var(--wire-color-secondary);
--link-tone-hover: var(--wire-color-secondary-hover);
}
.wire-link[data-color="success"] {
--link-tone: var(--wire-color-success);
--link-tone-hover: var(--wire-color-success);
}
.wire-link[data-color="warning"] {
--link-tone: var(--wire-color-warning-text);
--link-tone-hover: var(--wire-color-warning-text);
}
.wire-link[data-color="danger"] {
--link-tone: var(--wire-color-danger);
--link-tone-hover: var(--wire-color-danger);
}
.wire-link[data-color="info"] {
--link-tone: var(--wire-color-info);
--link-tone-hover: var(--wire-color-info);
}
.wire-link[data-color="neutral"] {
--link-tone: var(--wire-color-text);
--link-tone-hover: var(--wire-color-text);
}
.wire-link[data-size="xs"] {
font-size: 0.75rem;
}
.wire-link[data-size="md"] {
font-size: 1rem;
}
.wire-link[data-size="lg"] {
font-size: 1.125rem;
}
.wire-link[data-underline="always"] {
text-decoration: underline;
}
.wire-link[data-underline="hover"]:hover {
text-decoration: underline;
}
.wire-link:hover {
color: var(--link-tone-hover);
}
.wire-link:focus-visible {
outline: 2px solid var(--wire-color-focus);
outline-offset: 2px;
}
/* The label truncates; the outbound icon must not shrink with it. */
.wire-link__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-link__icon {
flex: 0 0 auto;
width: 0.875rem;
height: 0.875rem;
}
}
}
+62 -14
View File
@@ -11,26 +11,74 @@ component Typography {
view {
<div
data-ui-component="Typography"
class='wire-typography text-[var(--wire-color-text)] {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-none='maxWidth === "full"'
class:text-sm='size === "sm"'
class:text-base='size === "default" || size === "md"'
class:text-lg='size === "lg"'
class:columns-1='columns === 1'
class:md:columns-2='columns === 2'
class:lg:columns-3='columns === 3'
class:gap-4='gap === "sm"'
class:gap-8='gap === "md"'
class:gap-12='gap === "lg"'
class='wire-typography {class}'
data-size='{size}'
data-columns='{columns}'
data-gap='{gap}'
data-max-width='{maxWidth}'
>
<slot></slot>
</div>
}
style {
.wire-typography {
color: var(--wire-color-text);
font-size: 1rem;
}
.wire-typography[data-size="sm"] {
font-size: 0.875rem;
}
.wire-typography[data-size="lg"] {
font-size: 1.125rem;
}
.wire-typography[data-max-width="md"] {
max-width: 48rem;
}
.wire-typography[data-max-width="lg"] {
max-width: 64rem;
}
.wire-typography[data-max-width="xl"] {
max-width: 80rem;
}
.wire-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.
*/
.wire-typography[data-gap="sm"] {
column-gap: 1rem;
}
.wire-typography[data-gap="md"] {
column-gap: 2rem;
}
.wire-typography[data-gap="lg"] {
column-gap: 3rem;
}
@media (min-width: 768px) {
.wire-typography[data-columns="2"] {
column-count: 2;
}
}
@media (min-width: 1024px) {
.wire-typography[data-columns="3"] {
column-count: 3;
}
}
.wire-typography :where(h1, h2, h3, h4) {
color: var(--wire-color-text);
font-weight: 700;
+140 -24
View File
@@ -1,46 +1,162 @@
// Container -- a centred, width-limited page wrapper.
//
// <Container maxWidth="lg">...</Container>
//
// It also accepts columns and gap, because it always has and applications
// depend on it. Reach for Grid when a grid is the point; reach for Container
// when the point is the reading width.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Container {
props {
size: string = "default"
color: string = "primary"
// Stays 2 to match what shipped. A published default cannot be changed
// without silently reflowing every Container already in use.
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
centered: boolean = true
class: string = ""
}
functions {
shared function columnCount() {
var value = Number(columns)
if (!value || value < 1) {
return 1
}
return Math.min(6, value)
}
}
view {
<div
{...attrs}
data-ui-component="Container"
class='wire-container {class}'
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
data-max-width='{maxWidth}'
class='mx-auto w-full {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl" || maxWidth === "wide"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:px-4='size === "compact" || size === "default"'
class:px-5='size === "comfortable"'
class:px-6='size === "spacious"'
class:sm:px-6='size !== "compact"'
class:lg:px-8='size === "default" || size === "comfortable" || size === "spacious"'
class:grid='columns > 1'
class:grid-cols-1='columns > 1'
class:sm:grid-cols-2='columns === 2 || columns === 3 || columns === 4 || columns === 5 || columns === 6'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:lg:grid-cols-5='columns === 5'
class:lg:grid-cols-6='columns === 6'
class:gap-2='gap === "xs"'
class:gap-3='gap === "sm"'
class:gap-5='gap === "md"'
class:gap-8='gap === "lg"'
class:gap-10='gap === "xl"'
data-centered='{centered}'
data-columns='{columnCount()}'
>
<slot></slot>
</div>
}
style {
.wire-container {
--container-gap: 1.25rem;
width: 100%;
min-width: 0;
padding-inline: 1rem;
}
.wire-container[data-centered="true"] {
margin-inline: auto;
}
/* Reading widths, not breakpoints: the cap is about line length. */
.wire-container[data-max-width="md"] {
max-width: 48rem;
}
.wire-container[data-max-width="lg"] {
max-width: 64rem;
}
.wire-container[data-max-width="xl"],
.wire-container[data-max-width="wide"] {
max-width: 80rem;
}
.wire-container[data-max-width="2xl"] {
max-width: 96rem;
}
.wire-container[data-max-width="full"] {
max-width: none;
}
.wire-container[data-size="compact"] {
padding-inline: 1rem;
}
.wire-container[data-size="comfortable"] {
padding-inline: 1.25rem;
}
.wire-container[data-size="spacious"] {
padding-inline: 1.5rem;
}
.wire-container[data-gap="xs"] {
--container-gap: 0.5rem;
}
.wire-container[data-gap="sm"] {
--container-gap: 0.75rem;
}
.wire-container[data-gap="lg"] {
--container-gap: 2rem;
}
.wire-container[data-gap="xl"] {
--container-gap: 2.5rem;
}
/*
* Columns are opt-in. One column stays plain flow so a container does not
* quietly turn every page into a grid.
*/
.wire-container:not([data-columns="1"]) {
display: grid;
gap: var(--container-gap);
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.wire-container[data-columns="2"],
.wire-container[data-columns="3"],
.wire-container[data-columns="4"],
.wire-container[data-columns="5"],
.wire-container[data-columns="6"] {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wire-container[data-size="default"],
.wire-container[data-size="comfortable"],
.wire-container[data-size="spacious"] {
padding-inline: 1.5rem;
}
}
@media (min-width: 1024px) {
.wire-container[data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wire-container[data-columns="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.wire-container[data-columns="5"] {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.wire-container[data-columns="6"] {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.wire-container[data-size="default"],
.wire-container[data-size="comfortable"],
.wire-container[data-size="spacious"] {
padding-inline: 2rem;
}
}
}
}
+120 -48
View File
@@ -1,69 +1,141 @@
// Divider -- a rule between sections, optionally labelled.
//
// <Divider />
// <Divider label="or" />
// <Divider orientation="vertical" />
//
// The label sits between two rules rather than on top of one, so the text
// never overlaps the line at any width.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Divider {
props {
size: string = "default"
color: string = "primary"
label: string = ""
orientation: string = "horizontal"
variant: string = "solid"
class: string = ""
}
functions {
shared function isVertical() {
return orientation === "vertical"
}
// The rule only takes the component colour when there is a label. A plain
// divider is chrome and should stay the border token.
shared function tone() {
return label ? color : "border"
}
}
view {
<div
{...attrs}
data-ui-component="Divider"
class='wire-divider {class}'
data-size='{size}'
data-color='{color}'
data-tone='{tone()}'
data-variant='{variant}'
data-orientation='{orientation}'
class='w-full {class}'
class:h-full='orientation === "vertical"'
class:w-auto='orientation === "vertical"'
>
{#if orientation === "vertical"}
<div
data-labelled='{label ? "true" : "false"}'
role="separator"
aria-orientation="vertical"
class="mx-3 h-full min-h-6 border-l border-[var(--wire-color-border)]"
class:border-l-2='size === "lg"'
class:border-[var(--wire-color-primary)]='color === "primary"'
class:border-[var(--wire-color-secondary)]='color === "secondary"'
class:border-[var(--wire-color-success)]='color === "success"'
class:border-[var(--wire-color-warning)]='color === "warning"'
class:border-[var(--wire-color-danger)]='color === "danger"'
></div>
{:else}
<div
role="separator"
aria-orientation="horizontal"
class="flex w-full items-center gap-4"
aria-orientation='{isVertical() ? "vertical" : "horizontal"}'
>
<span
class="h-px flex-1 bg-[var(--wire-color-border)]"
class:h-0.5='size === "lg"'
class:bg-[var(--wire-color-primary)]='color === "primary" && label !== ""'
class:bg-[var(--wire-color-secondary)]='color === "secondary" && label !== ""'
class:bg-[var(--wire-color-success)]='color === "success" && label !== ""'
class:bg-[var(--wire-color-warning)]='color === "warning" && label !== ""'
class:bg-[var(--wire-color-danger)]='color === "danger" && label !== ""'
aria-hidden="true"
></span>
{#if label}
<span class="shrink-0 text-xs font-bold uppercase tracking-[0.16em] text-[var(--wire-color-text-muted)]">
{label}
</span>
<span
class="h-px flex-1 bg-[var(--wire-color-border)]"
class:h-0.5='size === "lg"'
class:bg-[var(--wire-color-primary)]='color === "primary"'
class:bg-[var(--wire-color-secondary)]='color === "secondary"'
class:bg-[var(--wire-color-success)]='color === "success"'
class:bg-[var(--wire-color-warning)]='color === "warning"'
class:bg-[var(--wire-color-danger)]='color === "danger"'
aria-hidden="true"
></span>
{/if}
</div>
{/if}
<span class="wire-divider__rule" aria-hidden="true"></span>
<span class="wire-divider__label" data-show="label">{label}</span>
<span class="wire-divider__rule" data-show="label" aria-hidden="true"></span>
</div>
}
style {
.wire-divider {
--divider-tone: var(--wire-color-border);
--divider-thickness: 1px;
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
min-width: 0;
}
.wire-divider[data-tone="primary"] {
--divider-tone: var(--wire-color-primary);
}
.wire-divider[data-tone="secondary"] {
--divider-tone: var(--wire-color-secondary);
}
.wire-divider[data-tone="success"] {
--divider-tone: var(--wire-color-success);
}
.wire-divider[data-tone="warning"] {
--divider-tone: var(--wire-color-warning);
}
.wire-divider[data-tone="danger"] {
--divider-tone: var(--wire-color-danger);
}
.wire-divider[data-size="lg"] {
--divider-thickness: 2px;
}
.wire-divider__rule {
flex: 1 1 auto;
height: var(--divider-thickness);
background: var(--divider-tone);
}
.wire-divider[data-variant="dashed"] .wire-divider__rule {
height: 0;
background: transparent;
border-top: var(--divider-thickness) dashed var(--divider-tone);
}
.wire-divider[data-variant="dotted"] .wire-divider__rule {
height: 0;
background: transparent;
border-top: var(--divider-thickness) dotted var(--divider-tone);
}
.wire-divider__label {
flex: 0 0 auto;
color: var(--wire-color-text-muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.wire-divider[data-orientation="vertical"] {
flex-direction: column;
width: auto;
height: 100%;
min-height: 1.5rem;
margin-inline: 0.75rem;
}
.wire-divider[data-orientation="vertical"] .wire-divider__rule {
width: var(--divider-thickness);
height: auto;
}
.wire-divider[data-orientation="vertical"][data-variant="dashed"] .wire-divider__rule {
width: 0;
border-top: 0;
border-left: var(--divider-thickness) dashed var(--divider-tone);
}
.wire-divider[data-orientation="vertical"][data-variant="dotted"] .wire-divider__rule {
width: 0;
border-top: 0;
border-left: var(--divider-thickness) dotted var(--divider-tone);
}
}
}
+117 -19
View File
@@ -1,3 +1,13 @@
// Grid -- an equal-track grid that collapses on small screens.
//
// <Grid columns={3} gap="lg">...</Grid>
//
// Pass minItemWidth to let the browser decide the count instead: the track
// list becomes auto-fit, which reflows continuously rather than at fixed
// breakpoints and suits card decks whose item count is not known.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Grid {
props {
size: string = "default"
@@ -5,36 +15,124 @@ component Grid {
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
// When set, the count is derived from the available width instead of the
// columns prop. Accepts any CSS length.
minItemWidth: string = ""
class: string = ""
}
functions {
shared function columnCount() {
var value = Number(columns)
if (!value || value < 1) {
return 1
}
return Math.min(6, value)
}
shared function autoTrack() {
return minItemWidth ? "repeat(auto-fit, minmax(" + minItemWidth + ", 1fr))" : ""
}
}
view {
<div
{...attrs}
data-ui-component="Grid"
class='wire-grid {class}'
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:sm:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:lg:grid-cols-5='columns === 5'
class:lg:grid-cols-6='columns === 6'
class:gap-2='gap === "xs"'
class:gap-3='gap === "sm"'
class:gap-5='gap === "md"'
class:gap-8='gap === "lg"'
class:gap-10='gap === "xl"'
class:items-start='size === "compact"'
class:items-stretch='size !== "compact"'
data-max-width='{maxWidth}'
data-columns='{columnCount()}'
data-auto='{minItemWidth ? "true" : "false"}'
style='--grid-auto-track:{autoTrack()};'
>
<slot></slot>
</div>
}
style {
.wire-grid {
--grid-gap: 1.25rem;
display: grid;
width: 100%;
min-width: 0;
gap: var(--grid-gap);
grid-template-columns: 1fr;
align-items: stretch;
}
.wire-grid[data-size="compact"] {
align-items: start;
}
.wire-grid[data-max-width="md"] {
max-width: 48rem;
}
.wire-grid[data-max-width="lg"] {
max-width: 64rem;
}
.wire-grid[data-max-width="xl"] {
max-width: 80rem;
}
.wire-grid[data-max-width="2xl"] {
max-width: 96rem;
}
.wire-grid[data-max-width="full"] {
max-width: none;
}
.wire-grid[data-gap="xs"] {
--grid-gap: 0.5rem;
}
.wire-grid[data-gap="sm"] {
--grid-gap: 0.75rem;
}
.wire-grid[data-gap="lg"] {
--grid-gap: 2rem;
}
.wire-grid[data-gap="xl"] {
--grid-gap: 2.5rem;
}
/*
* auto-fit needs no breakpoints, so it wins over the fixed counts below
* and applies at every width.
*/
.wire-grid[data-auto="true"] {
grid-template-columns: var(--grid-auto-track);
}
@media (min-width: 640px) {
.wire-grid[data-auto="false"]:not([data-columns="1"]) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.wire-grid[data-auto="false"][data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wire-grid[data-auto="false"][data-columns="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.wire-grid[data-auto="false"][data-columns="5"] {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.wire-grid[data-auto="false"][data-columns="6"] {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
}
}
}
+39
View File
@@ -3019,3 +3019,42 @@ test("custom scrollbar styles the scrollbar and drops its fake output", async ()
expect(style).toContain("--scrollbar-thickness");
expect(style).toContain("18rem");
});
test("layout components ship their own styles instead of Tailwind utilities", () => {
/*
* These were built from utility classes and class: conditionals. That works
* only where Tailwind is present, and a variant cost a dozen lines of
* conditionals. They now carry wire-* classes, a local style block, and
* data attributes the style block selects on.
*/
const migrated = [
"Container",
"Columns",
"Grid",
"Divider",
"Image",
"Link",
"Typography",
"Kbd",
"LayoutSplitter",
"CustomScrollbar",
];
const utility =
/class:(grid-cols-|sm:|lg:|md:|max-w-|gap-[0-9]|px-[0-9]|py-[0-9]|border-l|bg-\[|h-0|text-xs|items-)/;
for (const name of migrated) {
const source = readFileSync(uiComponentPath(name), "utf8");
expect({ name, hasStyleBlock: /^\s{2}style \{/m.test(source) }).toEqual({
name,
hasStyleBlock: true,
});
expect({ name, usesUtilityConditionals: utility.test(source) }).toEqual({
name,
usesUtilityConditionals: false,
});
expect({ name, hasWireClass: /class='wire-[a-z-]+/.test(source) }).toEqual({
name,
hasWireClass: true,
});
}
});