Compare commits

...
Author SHA1 Message Date
ClintchizandClaude Opus 5 b56cb8cba5 feat(ui): build LayoutSplitter and CustomScrollbar for real
Quality / quality (ubuntu-latest) (push) Failing after 6m8s
Quality / quality (windows-latest) (push) Canceled after 0s
Both advertised behaviour they did not have. LayoutSplitter declared
resizeStart, resize and resizeEnd with no pointer handling whatsoever, so a
caller wired up @resize and received nothing, for ever, with no error, and its
props were columns, gap and maxWidth copied from a grid scaffold.
CustomScrollbar was the same shape with a scroll output.

The splitter now resizes. Dragging lives in the reactive runtime behind
data-wrn-splitter, because a pointermove fires far too often to route through
a client function and a state write made in that callback is dropped; the
resolved size is held on the container as a --wrn-split custom property and
the component grids from it. The handle is a real separator: arrow keys step
it, Home and End go to the bounds rather than to nothing, and it carries
aria-valuenow, aria-valuemin and aria-valuemax. minSize fixes both bounds so
neither pane can be dragged away and left unrecoverable.

CustomScrollbar is CSS rather than script -- scrollbar-width and
scrollbar-color with webkit rules for the engines that still need them -- and
its fake scroll output is removed rather than left unimplemented, since a
caller can listen for a plain scroll event.

The test harness needed a fix too: mount did not bind the window CustomEvent,
so the runtime built events from the host global and happy-dom listeners never
matched them, which made anything dispatched look silently lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:22:45 +05:30
ClintchizandClaude Opus 5 b3a4d80df3 docs: layout and page-structure component group design
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:09:50 +05:30
ClintchizandClaude Opus 5 7bd4574b08 fix(example): theme the basic-app palette and surface the navigation tour
Quality / quality (ubuntu-latest) (push) Failing after 10m43s
Quality / quality (windows-latest) (push) Canceled after 0s
global.css defined its own fixed palette -- bg 0b1020, text e7ecff -- while
Wire UI surfaces follow the theme tokens. Switching to light turned the cards
light and left this text light with them, so the sign-in form rendered at a
contrast of about 1.1 and could not be read. The palette now derives from the
wire tokens, and the body wash is tinted from the primary token rather than a
fixed blue. Measured on the login form: light goes from 1.1 to 17.7, dark
stays at 18.2.

Anything an application hardcodes has to be themed as well, or it only ever
looks right in one mode.

The navigation tour is also reachable now: a Navigation entry in the site nav,
translated in both locales, and the page adopts the public layout so there is
a way back out of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 10:19:12 +05:30
ClintchizandClaude Opus 5 e32d83933e docs(example): one page wiring the whole navigation group together
Quality / quality (ubuntu-latest) (push) Failing after 13m3s
Quality / quality (windows-latest) (push) Canceled after 0s
examples/basic-app/app/pages/navigation.wrn puts all nine navigation
components in a single console shell instead of showing each alone: Navbar
with a nested dropdown, MegaMenu beside it, Breadcrumb, Sidebar as a rail that
becomes a Drawer, Tabs backed by a query parameter, a Stepper wizard,
Pagination, Scrollspy following the article, and Nav in the footer.

Three framework limits shaped the layout and are written into the page rather
than hidden:

  - object props are held in state and bound, because a brace at the start of
    an attribute is read as an interpolation
  - a shared function on a page is compiled standalone and cannot see page
    state by name, so state is passed as arguments
  - component props and slot content render once and do not track page state,
    so anything that has to react lives in page scope; Tabs reports the
    selection and the page owns what is shown

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 09:57:53 +05:30
ClintchizandClaude Opus 5 5ce2771718 fix(ui): release the scroll lock on closed drawers, add stepper wizard controls, slide tabs
Quality / quality (ubuntu-latest) (push) Failing after 12m29s
Quality / quality (windows-latest) (push) Canceled after 0s
The scroll lock was mine, and it broke every page carrying a Drawer or Modal.
Making dialog visibility testable, I replaced a size check with a data-show
check -- but a Drawer animates open, so its panel cannot be hidden with
data-show at all: display:none is not transitionable. Every closed Drawer
therefore looked open, took the body scroll lock and never released it, and
the page could not be scrolled. Both components publish data-open, which is
the signal that actually means open, and that is what is read now.

Stepper gains the wizard surface: showPanel renders each step body and shows
only the active one, the same contract Tabs uses, and controls adds Back,
Skip and Next, which becomes Finish on the last step. nextDisabled lets a form
hold the step; the component never validates anything itself, since the page
owns the form.

Stepper also gets a single root. The panels and controls were siblings of the
list, so the component had several roots and anything scoped to
data-ui-component missed most of it.

Tabs panels now slide in the direction of travel rather than fading upward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:41:18 +05:30
ClintchizandClaude Opus 5 68c7b96a9f fix(showcase): render object props, bridge the mega menu gap, make scrollspy testable
Quality / quality (ubuntu-latest) (push) Failing after 12m38s
Quality / quality (windows-latest) (push) Canceled after 0s
Object props never worked in generated demos, and my two earlier attempts each
traded one failure for another:

  - a bare {...} attribute is read by the compiler as an interpolation, so it
    parsed JSON as JavaScript and the page 500ed
  - parenthesising it compiled, but prop coercion runs JSON.parse on the raw
    attribute, so ({...}) threw and every demo rendered empty and silent
  - entity-escaping the braces did not help either: the compiler hands the
    attribute over without decoding, so JSON.parse still failed

They are now hoisted into page state and bound, which is what the playground
has always done. The state initialiser uses JSON.parse rather than an object
literal because the parser reads a leading brace as the start of a block.

Navbar gains a real profile: a brand, links, a two-column dropdown panel and
calls to action, instead of the generic scaffold samples that made every demo
look identical and showed no dropdown at all.

MegaMenu closed while the pointer travelled to it. The panel sits below the
trigger and that offset belongs to neither element, so crossing it fired
mouseleave on the root. A descendant now covers the gap.

Scrollspy could not be exercised at all: its links pointed at ids that did not
exist on the page. The demo now ships real sections, in page flow because the
runtime observes against the viewport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:12:37 +05:30
ClintchizandClaude Opus 5 6a9c48a207 perf(ui): remove dead runtime controllers and unused stylesheet families
Quality / quality (ubuntu-latest) (push) Failing after 6m17s
Quality / quality (windows-latest) (push) Canceled after 0s
Two orphaned controllers in the reactive runtime targeted markup nothing
emits any more: hydrateSidebarControllers looked for .wire-sidebar-shell and
friends, which the Sidebar rewrite replaced with BEM classes earlier today,
and hydrateDropdownControllers looked for [data-wrn-dropdown], which no
component or compiler output has ever produced.

ui.css loses the matching legacy sidebar rules, the wire-mega-menu family
left behind when the MegaMenu scaffold was replaced, and a set of
self-contained application-pattern families that nothing references.

Utility layers are deliberately kept even where an individual member is not
name-checked anywhere. wire-bg-primary is documented and tested while
wire-bg-secondary is not, but they are one public family and splitting them
would be incoherent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:46:42 +05:30
ClintchizandClaude Opus 5 9a03b7c8d4 merge: runtime observer consolidation and a shipped-size budget
Quality / quality (ubuntu-latest) (push) Failing after 11m0s
Quality / quality (windows-latest) (push) Canceled after 0s
One document observer with subscribers instead of four, runtime budgets
measured on minified output rather than raw source, Navbar styles moved into
the component, and two bugs fixed: object props broke showcase pages with a
500, and the runtime evaluated JSON sitting in a textarea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:20:34 +05:30
32 changed files with 2403 additions and 2039 deletions
+20 -2
View File
@@ -1,11 +1,29 @@
{ {
"version": "0.0.1", "version": "0.0.1",
"configurations": [ "configurations": [
{
"name": "basic-app",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"packages/cli/src/index.ts",
"dev",
"examples/basic-app",
"--port=3520"
],
"port": 3520
},
{ {
"name": "component-showcase", "name": "component-showcase",
"runtimeExecutable": "bun", "runtimeExecutable": "bun",
"runtimeArgs": ["run", "--cwd", "examples/component-showcase", "dev"], "runtimeArgs": [
"port": 3000 "run",
"packages/cli/src/index.ts",
"dev",
"examples/component-showcase",
"--port=3400"
],
"port": 3400
} }
] ]
} }
@@ -0,0 +1,178 @@
# Layout and page-structure components — design
Date: 2026-08-08
Status: approved, not yet implemented
## Problem
Two problems, in different proportions to the navigation group.
**Three components advertise behaviour they do not have.**
`LayoutSplitter` is the worst component in the library. It declares
`resizeStart`, `resize` and `resizeEnd`, so a caller reasonably wires up
`@resize` and receives nothing, forever, with no error. It has no pointer
handling at all, and its props are `columns`, `gap` and `maxWidth`, copied
wholesale from a grid scaffold. `CustomScrollbar` is the same shape: a
`scroll` output, no behaviour, and the same wrong grid props.
`Kbd` is nine lines, but that is not the same failure — a `<kbd>` element
genuinely does not need more. It is thin, not dishonest.
**Most of the group is built on Tailwind utilities.**
Seven of the ten use utility classes and `class:` conditionals rather than
`wire-*` classes and a local style block. They are theme-aware, because the
utilities carry arbitrary values bound to tokens
(`bg-[var(--wire-color-surface-soft)]`), but they depend on Tailwind being
present and produce a class explosion. `SectionHeader` has 38 such lines.
A correction that shaped this survey: when Tabs was rewritten it was described
as "the only component in the library using Tailwind utility classes". That
was wrong. **20 of 108 do**, and seven of those are in this group.
## Scope
The ten layout components plus the four page and section primitives, because
the goal is composing whole pages and the section-level pieces live in other
categories:
| | |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| Layout | Container, Columns, Grid, LayoutSplitter, Typography, Image, Link, Divider, Kbd, CustomScrollbar |
| Page and section | Section, SectionHeader, PageHeader, PublicPageShell |
`PageHeader` is already on the target convention — 696 lines, its own style
block, zero Tailwind. It is the model the rest migrate toward, and it needs an
audit rather than a rewrite.
## Decisions
### Styling
Every component in scope moves to `wire-*` classes with a local `style {}`
block and `data-*` attributes for variants, matching PageHeader, DataTable,
Toaster and the navigation group.
Variants become data attributes rather than `class:` conditionals, so
`variant="soft"` renders `data-variant="soft"` and the style block selects on
it. That replaces roughly a dozen `class:` lines per component with one
attribute and a handful of rules.
Tailwind stays available to applications. This is about the shipped library
not requiring it.
### LayoutSplitter
Real resizing, driven from the reactive runtime by a declarative attribute:
- `data-wrn-splitter="horizontal|vertical"` on the container
- `data-wrn-splitter-handle` on the divider
- the runtime writes the resolved size onto the container as a CSS custom
property, so the component styles from it
Pointer events fire far too often to route through client functions, and drag
state written inside a `pointermove` callback is dropped — the same constraint
that put roving focus and scrollspy in the runtime.
Keyboard support is part of the contract, not an extra: arrow keys move the
divider by a step, Home and End jump to the bounds, and the handle carries
`role="separator"` with `aria-valuenow`, `aria-valuemin` and `aria-valuemax`.
Sizes are clamped to a `minSize` per pane so a pane cannot be dragged to
nothing and become unrecoverable.
### CustomScrollbar
Scrollbar styling is CSS: `scrollbar-width` and `scrollbar-color` for the
standard property, with `::-webkit-scrollbar` rules for browsers that still
need them. No runtime, no scroll output — the existing `scroll` output is
removed rather than left unimplemented, since a caller can listen for a plain
`scroll` event on the element.
Its grid props (`columns`, `gap`, `maxWidth`) are replaced with ones that mean
something: `axis`, `thickness`, `thumb`, `track`, `maxHeight`.
### Kbd
Left as it is, beyond the `wire-*` migration. It is thin because a `<kbd>`
is thin.
### Runtime budget
The splitter is roughly 2k minified against 67.7k of an 80k budget. It fits.
The budget now measures minified output, so comments cost nothing.
## Components
| Component | Work |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| LayoutSplitter | Build. Runtime-driven pointer resize, keyboard steps, `role="separator"` with value attributes, per-pane `minSize`, real outputs |
| CustomScrollbar | Build. Themed scrollbar styling, meaningful props, the fake `scroll` output removed |
| Container | Migrate. `maxWidth`, `padding`, `centered` as data attributes |
| Columns | Migrate. `columns`, `gap`, responsive collapse in a media query rather than `md:` prefixes |
| Grid | Migrate. `columns`, `gap`, `minItemWidth` for an auto-fit track |
| Typography | Migrate the remaining Tailwind lines into its existing style block |
| Image | Migrate. `ratio`, `fit`, `rounded`, `loading` |
| Link | Migrate. `variant`, `underline`, external-link affordance |
| Divider | Migrate. `orientation`, `variant`, optional label |
| Kbd | Migrate only |
| Section | Migrate. `spacing`, `variant`, `color` as data attributes |
| SectionHeader | Migrate. Its 38 Tailwind lines collapse into a style block |
| PublicPageShell | Migrate. `minHeight`, `overflow`, `background`, `headerOffset` |
| PageHeader | Audit only — already on the convention |
Component count stays at 108; every file already exists.
## Data flow
Props in, outputs out, no global state. The splitter is the only component
with runtime state, and that state lives in the DOM as a custom property
rather than in component state.
## Error handling
Numeric props (`columns`, `minSize`, `thickness`) clamp rather than throw,
because they arrive as HTML attributes. A splitter with one child renders that
child full width instead of erroring.
## Testing
- Per-component entries in `packages/ui/test/ui.test.ts`
- Splitter drag and keyboard covered in `packages/csr/test/reactive.test.ts`,
using markers rather than measured size: the test DOM reports every element
as zero-sized
- Showcase profiles for each, so every component gets live demos
- Regenerate the component reference **before** the showcase — the generator
cannot see new props otherwise
- Each phase ends with `bun run check:production` green
## Phases
1. **Scaffolds** — splitter runtime and component, CustomScrollbar
2. **Layout migration** — Container, Columns, Grid, Divider, Image, Link,
Typography, Kbd
3. **Page and section** — Section, SectionHeader, PublicPageShell, PageHeader
audit, plus a page in `examples/basic-app` composing the whole set
## Risks
- The migration rewrites working markup. Visual regressions are the main
danger, and the browser pane cannot take screenshots, so verification is
computed styles and the UI visual contract rather than eyes on pixels.
- Removing `class:` conditionals changes the rendered class list, so any
application selecting on those utility classes breaks. This needs a
migration entry alongside the Tabs and Sidebar changes already outstanding.
## Codebase constraints to respect
- No apostrophes in `.wrn` comments
- `/* */` only inside style blocks; `//` silently eats the next CSS rule
- Block comments are not allowed inside `props {}` — line comments only
- `data-show` takes a bare expression, not `{...}`
- One root element per component
- No deferred state writes in client functions
- Object props are passed through state, never inline braces in an attribute
- Package components need explicit imports
- Stop the dev server before running tests; it wipes `.wrnexus`
- Restart the dev server after changing a package component
+6 -6
View File
@@ -29,7 +29,7 @@
"packages/ui/components/Container.wrn": "be8fce140043eced8b78c8dd14ff85ace4ed2d91671251641ef83e0fe4da8f2a", "packages/ui/components/Container.wrn": "be8fce140043eced8b78c8dd14ff85ace4ed2d91671251641ef83e0fe4da8f2a",
"packages/ui/components/ContextMenu.wrn": "2e011e9cb1c09a3a2344ed3fa29dcc74331cd39f838535d8aee4b249dceef0ce", "packages/ui/components/ContextMenu.wrn": "2e011e9cb1c09a3a2344ed3fa29dcc74331cd39f838535d8aee4b249dceef0ce",
"packages/ui/components/CopyMarkup.wrn": "8d57a7d72e02c126f855a181fc36e1b969edb90f683dce6b6ffe546d49d2b29b", "packages/ui/components/CopyMarkup.wrn": "8d57a7d72e02c126f855a181fc36e1b969edb90f683dce6b6ffe546d49d2b29b",
"packages/ui/components/CustomScrollbar.wrn": "4c1f7758b9cd47e20ecf922403e12b5b15b8127a2744383c6bd280ee20e5fdd1", "packages/ui/components/CustomScrollbar.wrn": "fedb2ab06cc2cf9499c130590945ca6971a3edfb60407b6658ef7756e4be9823",
"packages/ui/components/DataMap.wrn": "65552d73ecd1a148427dbb10d611f9352ca36bd490f595ab1e1a47e173445ba1", "packages/ui/components/DataMap.wrn": "65552d73ecd1a148427dbb10d611f9352ca36bd490f595ab1e1a47e173445ba1",
"packages/ui/components/DataTable.wrn": "5a76e50e8559c724aaf01ec954958ac640c99758d44f1eef8e6eb1cb6dc4f862", "packages/ui/components/DataTable.wrn": "5a76e50e8559c724aaf01ec954958ac640c99758d44f1eef8e6eb1cb6dc4f862",
"packages/ui/components/DatePicker.wrn": "18b07e59c27c2720bf960e9c04ad70d9e422ddcf0817fede74e71899811bb387", "packages/ui/components/DatePicker.wrn": "18b07e59c27c2720bf960e9c04ad70d9e422ddcf0817fede74e71899811bb387",
@@ -53,7 +53,7 @@
"packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123", "packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123",
"packages/ui/components/InputNumber.wrn": "a0aa4f4566bf54ffbef54c99648eab05b5f9870be6ea07639a4ecf3930afb631", "packages/ui/components/InputNumber.wrn": "a0aa4f4566bf54ffbef54c99648eab05b5f9870be6ea07639a4ecf3930afb631",
"packages/ui/components/Kbd.wrn": "d51fa614b36f971f885410785b481c2e6900b2416e6e7c8868dfda1ebd5f3b4c", "packages/ui/components/Kbd.wrn": "d51fa614b36f971f885410785b481c2e6900b2416e6e7c8868dfda1ebd5f3b4c",
"packages/ui/components/LayoutSplitter.wrn": "dac5a80acb76bf58dfd25233c68190c67d1aa1b3b0d4250a0957ea40912cf722", "packages/ui/components/LayoutSplitter.wrn": "a86dc54b74aed6b05e770cd1802960d6309bba42fe80e899e8aaa8b1b7aa88e3",
"packages/ui/components/LegendIndicator.wrn": "6a7607b27a17196f899132eb952203a1073e4077365a94935f485e700cbac665", "packages/ui/components/LegendIndicator.wrn": "6a7607b27a17196f899132eb952203a1073e4077365a94935f485e700cbac665",
"packages/ui/components/Link.wrn": "935ac24e8ebd067860dc9709ea9121710315ad6e02ce95cd5e982ae4d5978a02", "packages/ui/components/Link.wrn": "935ac24e8ebd067860dc9709ea9121710315ad6e02ce95cd5e982ae4d5978a02",
"packages/ui/components/List.wrn": "02f960a8e82fa049aac477bac9c1d9371394c86d9c47513138f61bffc79dc7d8", "packages/ui/components/List.wrn": "02f960a8e82fa049aac477bac9c1d9371394c86d9c47513138f61bffc79dc7d8",
@@ -61,7 +61,7 @@
"packages/ui/components/Map.wrn": "7b8a0d5412a464fd7cab8977d816542c506d8c5dab8230a3c984c2caee407c91", "packages/ui/components/Map.wrn": "7b8a0d5412a464fd7cab8977d816542c506d8c5dab8230a3c984c2caee407c91",
"packages/ui/components/MarketingSectionHeader.wrn": "7d6ffcc01a9331474475ea57ca58598be757abd9428b66bdfaf6f3603c5b145b", "packages/ui/components/MarketingSectionHeader.wrn": "7d6ffcc01a9331474475ea57ca58598be757abd9428b66bdfaf6f3603c5b145b",
"packages/ui/components/Marquee.wrn": "b2b35eedf3297ba12ab3776385a9f3eab001027648d87a2a1968f576eee6c025", "packages/ui/components/Marquee.wrn": "b2b35eedf3297ba12ab3776385a9f3eab001027648d87a2a1968f576eee6c025",
"packages/ui/components/MegaMenu.wrn": "48f4ced485df0e8de5217630c8bdcec0b2317bf04e6b279399f2c93d98d56995", "packages/ui/components/MegaMenu.wrn": "8a9a9b348ca1c5e182914ff55d981a91f47a49c52ef5e5efd8e4cb6e56361a43",
"packages/ui/components/MetricCard.wrn": "6451182739298691908f68258c0250cce2a78b0dc27c97115579ece30d7d9f92", "packages/ui/components/MetricCard.wrn": "6451182739298691908f68258c0250cce2a78b0dc27c97115579ece30d7d9f92",
"packages/ui/components/MetricGrid.wrn": "6018a98c10628ed240ed236ed916c0ff60994d192c3aadafd10bc876be0f9364", "packages/ui/components/MetricGrid.wrn": "6018a98c10628ed240ed236ed916c0ff60994d192c3aadafd10bc876be0f9364",
"packages/ui/components/Modal.wrn": "59d4ae9d6dd2700692d9edecfe53ee868e9f864363a4885961d1813cb2bee51f", "packages/ui/components/Modal.wrn": "59d4ae9d6dd2700692d9edecfe53ee868e9f864363a4885961d1813cb2bee51f",
@@ -85,11 +85,11 @@
"packages/ui/components/Sidebar.wrn": "03f1bbce75ca6d50ed940e62df39c539610e89eadd2064fe24abc7d5470f98bf", "packages/ui/components/Sidebar.wrn": "03f1bbce75ca6d50ed940e62df39c539610e89eadd2064fe24abc7d5470f98bf",
"packages/ui/components/SplitHero.wrn": "70e843565ff869bcf413b11b6d9830b2e2bd229863a00904596f71e2dff0e76d", "packages/ui/components/SplitHero.wrn": "70e843565ff869bcf413b11b6d9830b2e2bd229863a00904596f71e2dff0e76d",
"packages/ui/components/StatsBar.wrn": "c7d1df3895f25b6d3a2e1012a18b97592c7f33e65418b880d486f20c2d490686", "packages/ui/components/StatsBar.wrn": "c7d1df3895f25b6d3a2e1012a18b97592c7f33e65418b880d486f20c2d490686",
"packages/ui/components/Stepper.wrn": "09f216a44f888421091bd1bce760a076927500a573550d5b2b10c4f27f608e92", "packages/ui/components/Stepper.wrn": "df9093e4222ef3250963a8c6fbfec74477242735d23eabe896c29731c54fc14d",
"packages/ui/components/StrongPassword.wrn": "c7c5ef26ece6170dd7db9882f0dc98cb2e4607f1e5d43eb3fd39e5d15bbd3a2e", "packages/ui/components/StrongPassword.wrn": "c7c5ef26ece6170dd7db9882f0dc98cb2e4607f1e5d43eb3fd39e5d15bbd3a2e",
"packages/ui/components/StyledIcon.wrn": "4a4504e357dee9dd0418edbe85fc90b824ccdb3752123a2125da95b77bf0b336", "packages/ui/components/StyledIcon.wrn": "4a4504e357dee9dd0418edbe85fc90b824ccdb3752123a2125da95b77bf0b336",
"packages/ui/components/Switch.wrn": "ccb74599fab72b0d68b09a7f1f90b7732cb2f9cbed67a84219e897575ce30c28", "packages/ui/components/Switch.wrn": "ccb74599fab72b0d68b09a7f1f90b7732cb2f9cbed67a84219e897575ce30c28",
"packages/ui/components/Tabs.wrn": "4fa0c0854700532960043290700d7cf99663ec41692068101f0aae4c2b1a1181", "packages/ui/components/Tabs.wrn": "e38336d28876caa4e97da32a569749e3803b030284a85ce9f35fa788e51cc5b0",
"packages/ui/components/TextLink.wrn": "30782039293eb36d63b7b3a4f32a71a47177a3a68c7e90184be7cf7b4385eb19", "packages/ui/components/TextLink.wrn": "30782039293eb36d63b7b3a4f32a71a47177a3a68c7e90184be7cf7b4385eb19",
"packages/ui/components/Textarea.wrn": "ddf0b4f124b2cf0c0ab3d820d3ac0085f7c20466e977231949be264cd0cee8cf", "packages/ui/components/Textarea.wrn": "ddf0b4f124b2cf0c0ab3d820d3ac0085f7c20466e977231949be264cd0cee8cf",
"packages/ui/components/TimePicker.wrn": "2e8e7a90f6b6069a07e7ffd2725ba1e1031e84d55a4f1254025befbb314fa695", "packages/ui/components/TimePicker.wrn": "2e8e7a90f6b6069a07e7ffd2725ba1e1031e84d55a4f1254025befbb314fa695",
@@ -110,6 +110,6 @@
"packages/ui/components/progress.wrn": "ba6f4dfcc00f04f34e9533be675bb4a6200c3cdcd7d03fc597377e48520fdec7", "packages/ui/components/progress.wrn": "ba6f4dfcc00f04f34e9533be675bb4a6200c3cdcd7d03fc597377e48520fdec7",
"packages/ui/components/skeleton.wrn": "ceec8af147b8be08155500e378393ac7c72d819da61b62191c076c80e943d5a9", "packages/ui/components/skeleton.wrn": "ceec8af147b8be08155500e378393ac7c72d819da61b62191c076c80e943d5a9",
"packages/ui/components/spinner.wrn": "abc4ee3ede2e019289257e9009eee012c880a85839cc220fac6dbe1b780caf52", "packages/ui/components/spinner.wrn": "abc4ee3ede2e019289257e9009eee012c880a85839cc220fac6dbe1b780caf52",
"packages/ui/ui.css": "f71641efb0c98b673af70c10dbcea8e693de258ce8620b4f84c64553ea70204e" "packages/ui/ui.css": "3138b86f269b6f7e67922ef2c56918444d562b11f4ae6f1fab98b63805cd78dc"
} }
} }
@@ -11,6 +11,7 @@ component PublicLayout {
<a href="/chat">{t:nav.chat}</a> <a href="/chat">{t:nav.chat}</a>
<a href="/dashboard">{t:nav.dashboard}</a> <a href="/dashboard">{t:nav.dashboard}</a>
<a href="/about">{t:nav.about}</a> <a href="/about">{t:nav.about}</a>
<a href="/navigation">{t:nav.navigation}</a>
<button type="button" data-wire-theme-toggle aria-label="Toggle color theme">Theme</button> <button type="button" data-wire-theme-toggle aria-label="Toggle color theme">Theme</button>
<button class="wire-btn wire-btn--ghost wire-btn--sm" data-wire-lang-set="en">EN</button> <button class="wire-btn wire-btn--ghost wire-btn--sm" data-wire-lang-set="en">EN</button>
<button class="wire-btn wire-btn--ghost wire-btn--sm" data-wire-lang-set="es">ES</button> <button class="wire-btn wire-btn--ghost wire-btn--sm" data-wire-lang-set="es">ES</button>
+2 -1
View File
@@ -4,7 +4,8 @@
"ui": "UI", "ui": "UI",
"chat": "Chat", "chat": "Chat",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"about": "About" "about": "About",
"navigation": "Navigation"
}, },
"home": { "home": {
"title": "Hello from WrNexus", "title": "Hello from WrNexus",
+2 -1
View File
@@ -4,7 +4,8 @@
"ui": "UI", "ui": "UI",
"chat": "Chat", "chat": "Chat",
"dashboard": "Panel", "dashboard": "Panel",
"about": "Acerca de" "about": "Acerca de",
"navigation": "Navegación"
}, },
"home": { "home": {
"title": "Hola desde WrNexus", "title": "Hola desde WrNexus",
+438
View File
@@ -0,0 +1,438 @@
// Navigation tour. Route: /navigation
//
// One page that wires every navigation component together the way a real
// console would, rather than showing each in isolation:
//
// Navbar the top bar, with a dropdown built from nested children
// MegaMenu a wide product panel beside it
// Breadcrumb the trail under the bar
// Sidebar the left rail, which becomes a Drawer on a phone
// Tabs the main content, backed by a tab query parameter
// Stepper a wizard inside one tab, gated on a checkbox
// Pagination under the list in another tab
// Scrollspy a contents rail that follows the reader down the article
// Nav the secondary links in the footer
//
// Object props are held in state and bound. Passing one inline as an
// attribute would put a brace at the start of the value, which the compiler
// reads as an interpolation.
page NavigationTour {
layout = "public"
seo {
title = "Navigation tour"
description = "Every navigation component working together in one shell."
}
state brand = { label: "Acme", description: "Console", href: "/navigation" }
state topLinks = [
{ label: "Overview", href: "/navigation", value: "overview" },
{
label: "Workspace",
value: "workspace",
columns: 2,
description: "Everything your team touches.",
children: [
{
label: "Projects",
children: [
{ label: "Active", href: "/navigation", description: "In flight right now." },
{ label: "Archived", href: "/navigation", description: "Kept for the record." }
]
},
{
label: "People",
children: [
{ label: "Members", href: "/navigation", description: "Who can sign in." },
{ label: "Roles", href: "/navigation", description: "What they may do." }
]
}
]
},
{ label: "Reports", href: "/navigation", value: "reports" }
]
state topActions = [
{ label: "Docs", href: "/about", variant: "link" },
{ label: "New project", href: "/navigation", variant: "primary" }
]
state megaColumns = [
{
heading: "Platform",
items: [
{ label: "Runtime", href: "/navigation", description: "The browser half.", icon: "icon-[lucide--cpu]" },
{ label: "Compiler", href: "/navigation", description: "WRN to JavaScript.", icon: "icon-[lucide--hammer]" }
]
},
{
heading: "Tooling",
items: [
{ label: "CLI", href: "/navigation", description: "Scaffold and deploy.", icon: "icon-[lucide--terminal]" },
{ label: "Editor", href: "/navigation", description: "Language server.", icon: "icon-[lucide--code]" }
]
}
]
state crumbs = [
{ label: "Home", href: "/" },
{ label: "Console", href: "/navigation" },
{ label: "Navigation tour" }
]
state sideItems = [
{ label: "Overview", href: "#overview", value: "overview", icon: "icon-[lucide--gauge]" },
{
heading: "Build",
items: [
{ label: "Projects", href: "#projects", value: "projects", badge: "4" },
{
label: "Environments",
value: "envs",
items: [
{ label: "Production", href: "#projects", value: "prod" },
{ label: "Staging", href: "#projects", value: "staging" }
]
}
]
},
{
heading: "Account",
items: [{ label: "Settings", href: "#settings", value: "settings", icon: "icon-[lucide--settings]" }]
}
]
state contentTabs = [
{ label: "Article", value: "article", title: "Release notes" },
{ label: "Wizard", value: "wizard", title: "Create a project" },
{ label: "Members", value: "members", title: "Who can sign in" }
]
state spyLinks = [
{ label: "Overview", href: "#overview" },
{ label: "Projects", href: "#projects" },
{ label: "Settings", href: "#settings" }
]
state wizardSteps = [
{ label: "Details", title: "Name the project", content: "Pick something the team will recognise." },
{ label: "Access", title: "Who can see it", content: "Invite people now or later." },
{ label: "Review", title: "Check and create", content: "Nothing is created until you finish." }
]
state footerLinks = [
{ label: "Status", href: "/navigation", value: "status" },
{ label: "Changelog", href: "/navigation", value: "changelog" },
{ label: "Support", href: "/about", value: "support" }
]
state members = [
"A. Okafor", "R. Silva", "M. Chen", "J. Dubois", "P. Novak",
"T. Ahmed", "L. Rossi", "K. Bauer", "S. Haddad", "N. Ito"
]
// Tabs reports the selection and this page owns what is shown. Slot content
// and component props are rendered once and do not track page state, so
// anything that has to react lives out here in page scope.
state section = "article"
state sideActive = "overview"
state wizardStep = 0
// Not named page: that is the page keyword and shadows the runtime binding.
state currentPage = 1
state pageSize = 4
state lastEvent = "Nothing yet. Interact with any control."
functions {
client function onSideSelect(payload) {
sideActive = payload.value || ""
lastEvent = "Sidebar select: " + (payload.value || "(none)")
}
client function onTabChange(payload) {
section = payload.value
lastEvent = "Tab change: " + payload.value
}
// The stepper never validates anything itself. This page owns the
// checkbox, so it decides when the step may advance.
client function onWizardChange(payload) {
wizardStep = payload.index
lastEvent = "Wizard step: " + (payload.index + 1)
}
client function onWizardFinish() {
lastEvent = "Wizard finished. A real app would create the project here."
}
client function onWizardSkip(payload) {
wizardStep = payload.index
lastEvent = "Wizard skipped to step " + (payload.index + 1)
}
client function onPageChange(payload) {
currentPage = payload.page
lastEvent = "Page " + payload.page
}
client function onSpyChange(payload) {
lastEvent = "Reading: " + payload.label
}
client function onMegaSelect(payload) {
lastEvent = "Mega menu: " + payload.value
}
client function onNavSelect(payload) {
lastEvent = "Footer nav: " + payload.value
}
}
view {
<div class="tour">
<Navbar
brand={brand}
items={topLinks}
actions={topActions}
active="overview"
label="Primary navigation"
mobileLabel="Toggle navigation"
/>
<div class="tour__toolbar">
<MegaMenu
label="Products"
icon="icon-[lucide--box]"
columns={megaColumns}
footer="Not sure where to start? Talk to us."
@select="onMegaSelect(payload)"
/>
<Breadcrumb items={crumbs} />
</div>
<div class="tour__shell">
<aside class="tour__rail">
<Sidebar
items={sideItems}
active={sideActive}
label="Console"
mobileLabel="Open navigation"
drawerTitle="Console"
@select="onSideSelect(payload)"
/>
</aside>
<main class="tour__main">
<p class="tour__status" role="status">{lastEvent}</p>
<Tabs
items={contentTabs}
active="article"
mode="url"
param="tab"
label="Console sections"
@change="onTabChange(payload)"
/>
<section class="tour__panel" data-show="section === 'article'">
<section id="overview" class="tour__section">
<h3>Overview</h3>
<p>
Scroll this column and the contents rail on the right follows along.
The runtime observes these sections against the viewport and moves
aria-current to the matching link.
</p>
</section>
<section id="projects" class="tour__section">
<h3>Projects</h3>
<p>
The rail on the left is the Sidebar. Below the tablet breakpoint it
collapses behind a launcher and opens as a Drawer, which is where the
focus trap and the body scroll lock come from.
</p>
</section>
<section id="settings" class="tour__section">
<h3>Settings</h3>
<p>
The tab you are reading is mirrored into the URL. Switch tabs and the
address bar follows, then use the browser back button.
</p>
</section>
</section>
<section class="tour__panel" data-show="section === 'wizard'">
<Stepper
steps={wizardSteps}
active={wizardStep}
showPanel={true}
controls={true}
allowSkip={true}
nextDisabled={false}
backLabel="Back"
nextLabel="Next"
skipLabel="Skip"
finishLabel="Create project"
label="Create a project"
@change="onWizardChange(payload)"
@skip="onWizardSkip(payload)"
@finish="onWizardFinish()"
/>
<p class="tour__note">
Back, Skip and Next each emit an output, shown in the status line
above, and Next becomes Create project on the last step. The marker
does not advance here: active is a component prop, and props are
rendered once rather than tracking page state. Set nextDisabled to
hold Next while a form is incomplete.
</p>
</section>
<section class="tour__panel" data-show="section === 'members'">
<p class="tour__note">
Showing page {currentPage} of 3. Pagination reports the requested
page through its change output and the page decides what to do with
it; the counter above is bound to that state. Re-slicing the list
itself would need the list re-rendered, which an each block does not
do when state changes.
</p>
<ul class="tour__members">
{#each members as person}
<li>{person}</li>
{/each}
</ul>
<Pagination
page={currentPage}
pageSize={pageSize}
total={members.length}
variant="numbered"
@change="onPageChange(payload)"
/>
</section>
</main>
<aside class="tour__toc">
<Scrollspy
items={spyLinks}
heading="On this page"
active="#overview"
@change="onSpyChange(payload)"
/>
</aside>
</div>
<footer class="tour__footer">
<Nav
items={footerLinks}
active="status"
label="Secondary"
collapsible={false}
@select="onNavSelect(payload)"
/>
</footer>
</div>
}
style {
.tour {
display: grid;
gap: 1rem;
padding-bottom: 3rem;
}
.tour__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
/*
* Three columns: the rail, the reading column and the contents. The middle
* column carries min-width 0 so a wide table or code block inside a tab
* cannot push the whole shell sideways.
*/
.tour__shell {
display: grid;
grid-template-columns: 15rem minmax(0, 1fr) 12rem;
gap: 1.5rem;
align-items: start;
}
.tour__main {
min-width: 0;
}
.tour__rail,
.tour__toc {
position: sticky;
top: 1rem;
}
.tour__status {
margin: 0 0 0.75rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface-soft);
color: var(--wire-color-text-muted);
font-size: 0.82rem;
}
/* Tall enough that scrolling actually moves between sections. */
.tour__section {
min-height: 60vh;
padding-top: 0.5rem;
scroll-margin-top: 1rem;
}
.tour__section h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: 700;
}
.tour__section p {
margin: 0;
max-width: 46rem;
color: var(--wire-color-text-muted);
line-height: 1.7;
}
.tour__members {
display: grid;
gap: 0.35rem;
margin: 0 0 1rem;
padding: 0;
list-style: none;
}
.tour__members li {
padding: 0.5rem 0.75rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
font-size: 0.88rem;
}
.tour__footer {
padding-top: 1rem;
border-top: 1px solid var(--wire-color-border);
}
/* The rails fold away first; the reading column is what matters. */
@media (max-width: 1023px) {
.tour__shell {
grid-template-columns: minmax(0, 1fr);
}
.tour__rail,
.tour__toc {
position: static;
}
}
}
}
+3
View File
@@ -12,6 +12,7 @@ export interface Routes {
"/language-tools": Record<string, never>; "/language-tools": Record<string, never>;
"/login": Record<string, never>; "/login": Record<string, never>;
"/modal": Record<string, never>; "/modal": Record<string, never>;
"/navigation": Record<string, never>;
"/partial-static": Record<string, never>; "/partial-static": Record<string, never>;
"/platform-showcase": Record<string, never>; "/platform-showcase": Record<string, never>;
"/reactive": Record<string, never>; "/reactive": Record<string, never>;
@@ -32,6 +33,7 @@ export interface RouteNames {
"language.tools": "/language-tools"; "language.tools": "/language-tools";
"login": "/login"; "login": "/login";
"modal": "/modal"; "modal": "/modal";
"navigation": "/navigation";
"partial.static": "/partial-static"; "partial.static": "/partial-static";
"platform.showcase": "/platform-showcase"; "platform.showcase": "/platform-showcase";
"reactive": "/reactive"; "reactive": "/reactive";
@@ -121,6 +123,7 @@ export function route<N extends RouteName>(
"language.tools": "/language-tools", "language.tools": "/language-tools",
"login": "/login", "login": "/login",
"modal": "/modal", "modal": "/modal",
"navigation": "/navigation",
"partial.static": "/partial-static", "partial.static": "/partial-static",
"platform.showcase": "/platform-showcase", "platform.showcase": "/platform-showcase",
"reactive": "/reactive", "reactive": "/reactive",
+20 -6
View File
@@ -13,12 +13,21 @@
/* --- App theme layered on top of Tailwind's utilities & preflight --- */ /* --- App theme layered on top of Tailwind's utilities & preflight --- */
/*
* The app palette is derived from the theme tokens rather than fixed.
*
* These used to be hardcoded dark values. Wire UI surfaces follow the theme,
* so switching to light turned the cards light while this text stayed light
* too -- the sign-in form rendered at a contrast of about 1.1 and was
* unreadable. Anything an app hardcodes here has to be themed as well, or it
* only ever looks right in one mode.
*/
:root { :root {
--bg: #0b1020; --bg: var(--wire-color-bg);
--surface: #141a30; --surface: var(--wire-color-surface);
--text: #e7ecff; --text: var(--wire-color-text);
--muted: #9aa6d0; --muted: var(--wire-color-text-muted);
--brand: #6c8cff; --brand: var(--wire-color-primary);
--radius: 10px; --radius: 10px;
--maxw: 720px; --maxw: 720px;
} }
@@ -36,7 +45,12 @@ body {
Roboto, Roboto,
sans-serif; sans-serif;
color: var(--text); color: var(--text);
background: radial-gradient(1200px 600px at 50% -10%, #1b2750 0%, var(--bg) 60%); /* Tinted from the primary token so the wash follows the theme too. */
background: radial-gradient(
1200px 600px at 50% -10%,
color-mix(in srgb, var(--wire-color-primary) 18%, var(--bg)) 0%,
var(--bg) 60%
);
background-color: var(--bg); background-color: var(--bg);
} }
@@ -1,11 +1,12 @@
// Generated by scripts/generate-showcase.mjs. Do not edit directly. // Generated by scripts/generate-showcase.mjs. Do not edit directly.
page CustomScrollbarDetail { page CustomScrollbarDetail {
layout = "showcase" layout = "showcase"
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary" state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary"
state playground_columns = Number(ctx.url.searchParams.get("pg_columns") ?? "2") state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_gap = ctx.url.searchParams.get("pg_gap") ?? "md" state playground_axis = ctx.url.searchParams.get("pg_axis") ?? "vertical"
state playground_maxWidth = ctx.url.searchParams.get("pg_maxWidth") ?? "xl" state playground_thickness = Number(ctx.url.searchParams.get("pg_thickness") ?? "8")
state playground_maxHeight = ctx.url.searchParams.get("pg_maxHeight") ?? "20rem"
state playground_radius = ctx.url.searchParams.get("pg_radius") ?? "999px"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1" state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo { seo {
title = "Custom Scrollbar" title = "Custom Scrollbar"
@@ -20,26 +21,26 @@ page CustomScrollbarDetail {
<span class="showcase-eyebrow">Layout component</span> <span class="showcase-eyebrow">Layout component</span>
<h1>Custom Scrollbar</h1> <h1>Custom Scrollbar</h1>
<p>Theme-aware, responsive custom scrollbar component.</p> <p>Theme-aware, responsive custom scrollbar component.</p>
<div class="detail-badges"><span>6 props</span><span>1 slots</span><span>1 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>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div> <div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header> </header>
<section id="playground" class="detail-section playground-section" data-playground data-playground-component="CustomScrollbar" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="scroll"> <section id="playground" class="detail-section playground-section" data-playground data-playground-component="CustomScrollbar" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="">
<div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Custom Scrollbar</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div> <div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Custom Scrollbar</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div>
<div class="playground-workbench"> <div class="playground-workbench">
<div class="playground-preview"> <div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div> <div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><CustomScrollbar size='{playground_size}' color='{playground_color}' columns='{playground_columns}' gap='{playground_gap}' maxWidth='{playground_maxWidth}' class='{playground_class}' /></div> <div class="playground-preview-source" data-playground-preview><CustomScrollbar color='{playground_color}' size='{playground_size}' axis='{playground_axis}' thickness='{playground_thickness}' maxHeight='{playground_maxHeight}' radius='{playground_radius}' class='{playground_class}'><div class="showcase-scroll-demo"><p>Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p></div></CustomScrollbar></div>
<section class="playground-code" aria-label="Current component code"> <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> <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;CustomScrollbar /&gt;</code></pre> <pre><code data-playground-code>&lt;CustomScrollbar /&gt;</code></pre>
</section> </section>
<div class="playground-status" data-playground-status aria-live="polite"></div> <div class="playground-status" data-playground-status aria-live="polite"></div>
<div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div>
</div> </div>
<form class="playground-controls" data-playground-form> <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> <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-custom-scrollbar-size"><span><strong>size</strong><small>string</small></span><select id="playground-custom-scrollbar-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-custom-scrollbar-color"><span><strong>color</strong><small>string</small></span><select id="playground-custom-scrollbar-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-custom-scrollbar-columns"><span><strong>columns</strong><small>number</small></span><input id="playground-custom-scrollbar-columns" name="pg_columns" type="number" value="2" /></label><label class="playground-field" for="playground-custom-scrollbar-gap"><span><strong>gap</strong><small>string</small></span><select id="playground-custom-scrollbar-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-custom-scrollbar-maxWidth"><span><strong>max Width</strong><small>string</small></span><select id="playground-custom-scrollbar-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-custom-scrollbar-class"><span><strong>class</strong><small>string</small></span><input id="playground-custom-scrollbar-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div> <div class="playground-fields"><label class="playground-field" for="playground-custom-scrollbar-color"><span><strong>color</strong><small>string</small></span><select id="playground-custom-scrollbar-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-custom-scrollbar-size"><span><strong>size</strong><small>string</small></span><select id="playground-custom-scrollbar-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-custom-scrollbar-axis"><span><strong>axis</strong><small>string</small></span><input id="playground-custom-scrollbar-axis" name="pg_axis" type="text" value="vertical" /></label><label class="playground-field" for="playground-custom-scrollbar-thickness"><span><strong>thickness</strong><small>number</small></span><input id="playground-custom-scrollbar-thickness" name="pg_thickness" type="number" value="8" /></label><label class="playground-field" for="playground-custom-scrollbar-maxHeight"><span><strong>max Height</strong><small>string</small></span><input id="playground-custom-scrollbar-maxHeight" name="pg_maxHeight" type="text" value="20rem" /></label><label class="playground-field" for="playground-custom-scrollbar-radius"><span><strong>radius</strong><small>string</small></span><input id="playground-custom-scrollbar-radius" name="pg_radius" type="text" value="999px" /></label><label class="playground-field" for="playground-custom-scrollbar-class"><span><strong>class</strong><small>string</small></span><input id="playground-custom-scrollbar-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form> </form>
</div> </div>
</section> </section>
@@ -48,72 +49,54 @@ page CustomScrollbarDetail {
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list"> <section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Recommended</span><h2>Production default</h2><p>Balanced spacing, hierarchy, and content for the most common product workflow.</p></div> <div><span class="demo-case-eyebrow">Recommended</span><h2>Themed scrolling region</h2><p>Scrollbar appearance is CSS rather than script: scrollbar-width and scrollbar-color are the standard properties, with webkit rules for the engines that still need them. The thumb follows the component colour.</p></div>
<span class="demo-case-number">01</span> <span class="demo-case-number">01</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="custom-scrollbar-demo-1" class="demo-canvas demo-canvas--1"> <div id="custom-scrollbar-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><CustomScrollbar size="default" columns="2" class="showcase-instance showcase-instance--1" /></div> <div class="demo-render"><CustomScrollbar size="default" axis="vertical" thickness="8" maxHeight="14rem" radius="999px" class="showcase-instance showcase-instance--1"><div class="showcase-scroll-demo"><p>Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p></div></CustomScrollbar></div>
</div> </div>
<section class="demo-code" aria-label="Production default usage"> <section class="demo-code" aria-label="Themed scrolling region usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;CustomScrollbar /&gt;</code></pre> <pre><code>&lt;CustomScrollbar
maxHeight="14rem"&gt;
&lt;div class="showcase-scroll-demo"&gt;&lt;p&gt;Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;p&gt;Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.&lt;/p&gt;&lt;/div&gt;
&lt;/CustomScrollbar&gt;</code></pre>
</section> </section>
</div> </div>
</article> </article>
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Dense UI</span><h2>Compact application</h2><p>A tighter variation for dashboards, side panels, tables, and operational interfaces.</p></div> <div><span class="demo-case-eyebrow">Advanced</span><h2>Horizontal, thicker track</h2><p>A horizontal region with a heavier track. thickness is clamped, since it arrives as an attribute and a scrollbar wider than its content helps nobody.</p></div>
<span class="demo-case-number">02</span> <span class="demo-case-number">02</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="custom-scrollbar-demo-2" class="demo-canvas demo-canvas--2"> <div id="custom-scrollbar-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><CustomScrollbar size="sm" columns="2" class="showcase-instance showcase-instance--2" /></div> <div class="demo-render"><CustomScrollbar color="info" size="sm" axis="horizontal" thickness="14" maxHeight="8rem" radius="4px" class="showcase-instance showcase-instance--2"><div class="showcase-scroll-demo showcase-scroll-demo--wide"><span>Column 1</span><span>Column 2</span><span>Column 3</span><span>Column 4</span><span>Column 5</span><span>Column 6</span><span>Column 7</span><span>Column 8</span><span>Column 9</span><span>Column 10</span><span>Column 11</span><span>Column 12</span><span>Column 13</span><span>Column 14</span></div></CustomScrollbar></div>
</div> </div>
<section class="demo-code" aria-label="Compact application usage"> <section class="demo-code" aria-label="Horizontal, thicker track usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;CustomScrollbar <pre><code>&lt;CustomScrollbar
color="info"
size="sm" size="sm"
/&gt;</code></pre> axis="horizontal"
</section> thickness="14"
</div> maxHeight="8rem"
</article> radius="4px"&gt;
<article class="demo-case"> &lt;div class="showcase-scroll-demo showcase-scroll-demo--wide"&gt;&lt;span&gt;Column 1&lt;/span&gt;&lt;span&gt;Column 2&lt;/span&gt;&lt;span&gt;Column 3&lt;/span&gt;&lt;span&gt;Column 4&lt;/span&gt;&lt;span&gt;Column 5&lt;/span&gt;&lt;span&gt;Column 6&lt;/span&gt;&lt;span&gt;Column 7&lt;/span&gt;&lt;span&gt;Column 8&lt;/span&gt;&lt;span&gt;Column 9&lt;/span&gt;&lt;span&gt;Column 10&lt;/span&gt;&lt;span&gt;Column 11&lt;/span&gt;&lt;span&gt;Column 12&lt;/span&gt;&lt;span&gt;Column 13&lt;/span&gt;&lt;span&gt;Column 14&lt;/span&gt;&lt;/div&gt;
<header class="demo-case-header"> &lt;/CustomScrollbar&gt;</code></pre>
<div><span class="demo-case-eyebrow">Extended</span><h2>Rich configuration</h2><p>A more expressive variation using additional data, stronger emphasis, and optional states.</p></div>
<span class="demo-case-number">03</span>
</header>
<div class="demo-workbench">
<div id="custom-scrollbar-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"><CustomScrollbar size="lg" columns="2" class="showcase-instance showcase-instance--3" /></div>
</div>
<section class="demo-code" aria-label="Rich configuration 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;CustomScrollbar
size="lg"
/&gt;</code></pre>
</section> </section>
</div> </div>
</article></div></section> </article></div></section>
<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@scroll</code><span>Fires when the component emits the scroll event.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;CustomScrollbar
@scroll='console.log(payload)'
/&gt;</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Direct output handlers</span><small>.js</small></header><pre><code>import <span>&#123;</span> registerOutputHandler <span>&#125;</span> from "@wrnexus/csr/outputs"
const component = document.querySelector("[data-ui-component=\"CustomScrollbar\"], [data-component=\"CustomScrollbar\"]") <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>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>axis</code></td><td>string</td><td><code>"vertical"</code></td><td>No</td></tr><tr><td><code>thickness</code></td><td>number</td><td><code>8</code></td><td>No</td></tr><tr><td><code>maxHeight</code></td><td>string</td><td><code>"20rem"</code></td><td>No</td></tr><tr><td><code>radius</code></td><td>string</td><td><code>"999px"</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>
if (component) registerOutputHandler(component, "scroll", (payload) =&gt; <span>&#123;</span>
console.log("scroll", payload)
<span>&#125;</span>)</code></pre></section></div></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>
</main> </main>
<aside class="detail-aside"> <aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#custom-scrollbar-demo-1">Production default</a><a href="#custom-scrollbar-demo-2">Compact application</a><a href="#custom-scrollbar-demo-3">Rich configuration</a><a href="#events">Outputs</a><a href="#api">Props API</a></div> <div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#custom-scrollbar-demo-1">Themed scrolling region</a><a href="#custom-scrollbar-demo-2">Horizontal, thicker track</a><a href="#api">Props API</a></div>
</aside> </aside>
</div> </div>
<nav class="detail-pagination"> <nav class="detail-pagination">
@@ -1,11 +1,12 @@
// Generated by scripts/generate-showcase.mjs. Do not edit directly. // Generated by scripts/generate-showcase.mjs. Do not edit directly.
page LayoutSplitterDetail { page LayoutSplitterDetail {
layout = "showcase" layout = "showcase"
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary" state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary"
state playground_columns = Number(ctx.url.searchParams.get("pg_columns") ?? "2") state playground_size = Number(ctx.url.searchParams.get("pg_size") ?? "50")
state playground_gap = ctx.url.searchParams.get("pg_gap") ?? "md" state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal"
state playground_maxWidth = ctx.url.searchParams.get("pg_maxWidth") ?? "xl" state playground_minSize = Number(ctx.url.searchParams.get("pg_minSize") ?? "15")
state playground_step = Number(ctx.url.searchParams.get("pg_step") ?? "5")
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Layout Splitter"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1" state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo { seo {
title = "Layout Splitter" title = "Layout Splitter"
@@ -20,26 +21,34 @@ page LayoutSplitterDetail {
<span class="showcase-eyebrow">Layout component</span> <span class="showcase-eyebrow">Layout component</span>
<h1>Layout Splitter</h1> <h1>Layout Splitter</h1>
<p>Theme-aware, responsive layout splitter component.</p> <p>Theme-aware, responsive layout splitter component.</p>
<div class="detail-badges"><span>6 props</span><span>1 slots</span><span>3 outputs</span><span>Theme ready</span><span>Responsive</span></div> <div class="detail-badges"><span>7 props</span><span>3 slots</span><span>1 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div> </div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div> <div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header> </header>
<section id="playground" class="detail-section playground-section" data-playground data-playground-component="LayoutSplitter" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="resizeStart,resize,resizeEnd"> <section id="playground" class="detail-section playground-section" data-playground data-playground-component="LayoutSplitter" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="resize">
<div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Layout Splitter</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div> <div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Layout Splitter</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div>
<div class="playground-workbench"> <div class="playground-workbench">
<div class="playground-preview"> <div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div> <div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><LayoutSplitter size='{playground_size}' color='{playground_color}' columns='{playground_columns}' gap='{playground_gap}' maxWidth='{playground_maxWidth}' class='{playground_class}' /></div> <div class="playground-preview-source" data-playground-preview><LayoutSplitter color='{playground_color}' size='{playground_size}' orientation='{playground_orientation}' minSize='{playground_minSize}' step='{playground_step}' label='{playground_label}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code"> <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> <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;LayoutSplitter /&gt;</code></pre> <pre><code data-playground-code>&lt;LayoutSplitter
label="Layout Splitter"&gt;
&lt;div data-slot="start"&gt;
&lt;div class="showcase-slot"&gt;start slot&lt;/div&gt;
&lt;/div&gt;
&lt;div data-slot="end"&gt;
&lt;div class="showcase-slot"&gt;end slot&lt;/div&gt;
&lt;/div&gt;
&lt;/LayoutSplitter&gt;</code></pre>
</section> </section>
<div class="playground-status" data-playground-status aria-live="polite"></div> <div class="playground-status" data-playground-status aria-live="polite"></div>
<div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div> <div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div>
</div> </div>
<form class="playground-controls" data-playground-form> <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> <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-layout-splitter-size"><span><strong>size</strong><small>string</small></span><select id="playground-layout-splitter-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-layout-splitter-color"><span><strong>color</strong><small>string</small></span><select id="playground-layout-splitter-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-layout-splitter-columns"><span><strong>columns</strong><small>number</small></span><input id="playground-layout-splitter-columns" name="pg_columns" type="number" value="2" /></label><label class="playground-field" for="playground-layout-splitter-gap"><span><strong>gap</strong><small>string</small></span><select id="playground-layout-splitter-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-layout-splitter-maxWidth"><span><strong>max Width</strong><small>string</small></span><select id="playground-layout-splitter-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-layout-splitter-class"><span><strong>class</strong><small>string</small></span><input id="playground-layout-splitter-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div> <div class="playground-fields"><label class="playground-field" for="playground-layout-splitter-color"><span><strong>color</strong><small>string</small></span><select id="playground-layout-splitter-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-layout-splitter-size"><span><strong>size</strong><small>number</small></span><input id="playground-layout-splitter-size" name="pg_size" type="number" value="50" /></label><label class="playground-field" for="playground-layout-splitter-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-layout-splitter-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-field" for="playground-layout-splitter-minSize"><span><strong>min Size</strong><small>number</small></span><input id="playground-layout-splitter-minSize" name="pg_minSize" type="number" value="15" /></label><label class="playground-field" for="playground-layout-splitter-step"><span><strong>step</strong><small>number</small></span><input id="playground-layout-splitter-step" name="pg_step" type="number" value="5" /></label><label class="playground-field" for="playground-layout-splitter-label"><span><strong>label</strong><small>string</small></span><input id="playground-layout-splitter-label" name="pg_label" type="text" value="Layout Splitter" /></label><label class="playground-field" for="playground-layout-splitter-class"><span><strong>class</strong><small>string</small></span><input id="playground-layout-splitter-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form> </form>
</div> </div>
</section> </section>
@@ -48,82 +57,72 @@ page LayoutSplitterDetail {
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list"> <section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Recommended</span><h2>Production default</h2><p>Balanced spacing, hierarchy, and content for the most common product workflow.</p></div> <div><span class="demo-case-eyebrow">Recommended</span><h2>Drag or arrow the divider</h2><p>Two panes with a movable divider. Drag it, or focus it and use the arrow keys; Home and End go to the bounds. The size is resolved in the runtime and written onto the container as a custom property, because a pointermove fires far too often to route through a client function.</p></div>
<span class="demo-case-number">01</span> <span class="demo-case-number">01</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="layout-splitter-demo-1" class="demo-canvas demo-canvas--1"> <div id="layout-splitter-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><LayoutSplitter size="default" columns="2" class="showcase-instance showcase-instance--1" /></div> <div class="demo-render"><LayoutSplitter size="40" orientation="horizontal" minSize="20" step="5" label="Resize panels" class="showcase-instance showcase-instance--1" /></div>
</div> </div>
<section class="demo-code" aria-label="Production default usage"> <section class="demo-code" aria-label="Drag or arrow the divider usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;LayoutSplitter /&gt;</code></pre> <pre><code>&lt;LayoutSplitter
size="40"
minSize="20"&gt;
&lt;div data-slot="start"&gt;
&lt;div class="showcase-slot"&gt;start slot&lt;/div&gt;
&lt;/div&gt;
&lt;div data-slot="end"&gt;
&lt;div class="showcase-slot"&gt;end slot&lt;/div&gt;
&lt;/div&gt;
&lt;/LayoutSplitter&gt;</code></pre>
</section> </section>
</div> </div>
</article> </article>
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Dense UI</span><h2>Compact application</h2><p>A tighter variation for dashboards, side panels, tables, and operational interfaces.</p></div> <div><span class="demo-case-eyebrow">Advanced</span><h2>Stacked panes</h2><p>Vertical orientation splits top from bottom and switches the arrow keys to up and down. Below the small breakpoint both orientations stack, because two panes side by side stop making sense on a phone.</p></div>
<span class="demo-case-number">02</span> <span class="demo-case-number">02</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="layout-splitter-demo-2" class="demo-canvas demo-canvas--2"> <div id="layout-splitter-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><LayoutSplitter size="sm" columns="2" class="showcase-instance showcase-instance--2" /></div> <div class="demo-render"><LayoutSplitter size="60" orientation="vertical" minSize="25" step="10" label="Resize rows" class="showcase-instance showcase-instance--2" /></div>
</div> </div>
<section class="demo-code" aria-label="Compact application usage"> <section class="demo-code" aria-label="Stacked panes usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;LayoutSplitter <pre><code>&lt;LayoutSplitter
size="sm" size="60"
/&gt;</code></pre> orientation="vertical"
</section> minSize="25"
</div> step="10"
</article> label="Resize rows"&gt;
<article class="demo-case"> &lt;div data-slot="start"&gt;
<header class="demo-case-header"> &lt;div class="showcase-slot"&gt;start slot&lt;/div&gt;
<div><span class="demo-case-eyebrow">Extended</span><h2>Rich configuration</h2><p>A more expressive variation using additional data, stronger emphasis, and optional states.</p></div> &lt;/div&gt;
<span class="demo-case-number">03</span> &lt;div data-slot="end"&gt;
</header> &lt;div class="showcase-slot"&gt;end slot&lt;/div&gt;
<div class="demo-workbench"> &lt;/div&gt;
<div id="layout-splitter-demo-3" class="demo-canvas demo-canvas--3"> &lt;/LayoutSplitter&gt;</code></pre>
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><LayoutSplitter size="lg" columns="2" class="showcase-instance showcase-instance--3" /></div>
</div>
<section class="demo-code" aria-label="Rich configuration 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;LayoutSplitter
size="lg"
/&gt;</code></pre>
</section> </section>
</div> </div>
</article></div></section> </article></div></section>
<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@resizeStart</code><span>Fires when the component emits the resizeStart event.</span></div><div><code>@resize</code><span>Fires when the component emits the resize event.</span></div><div><code>@resizeEnd</code><span>Fires when the component emits the resizeEnd event.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;LayoutSplitter <section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@resize</code><span>Fires when the component emits the resize event.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;LayoutSplitter
@resizeStart='console.log(payload)'
@resize='console.log(payload)' @resize='console.log(payload)'
@resizeEnd='console.log(payload)'
/&gt;</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Direct output handlers</span><small>.js</small></header><pre><code>import <span>&#123;</span> registerOutputHandler <span>&#125;</span> from "@wrnexus/csr/outputs" /&gt;</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Direct output handlers</span><small>.js</small></header><pre><code>import <span>&#123;</span> registerOutputHandler <span>&#125;</span> from "@wrnexus/csr/outputs"
const component = document.querySelector("[data-ui-component=\"LayoutSplitter\"], [data-component=\"LayoutSplitter\"]") const component = document.querySelector("[data-ui-component=\"LayoutSplitter\"], [data-component=\"LayoutSplitter\"]")
if (component) registerOutputHandler(component, "resizeStart", (payload) =&gt; <span>&#123;</span>
console.log("resizeStart", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "resize", (payload) =&gt; <span>&#123;</span> if (component) registerOutputHandler(component, "resize", (payload) =&gt; <span>&#123;</span>
console.log("resize", payload) console.log("resize", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "resizeEnd", (payload) =&gt; <span>&#123;</span>
console.log("resizeEnd", payload)
<span>&#125;</span>)</code></pre></section></div></div></section> <span>&#125;</span>)</code></pre></section></div></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>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>number</td><td><code>50</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>minSize</code></td><td>number</td><td><code>15</code></td><td>No</td></tr><tr><td><code>step</code></td><td>number</td><td><code>5</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Resize panels"</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> </main>
<aside class="detail-aside"> <aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#layout-splitter-demo-1">Production default</a><a href="#layout-splitter-demo-2">Compact application</a><a href="#layout-splitter-demo-3">Rich configuration</a><a href="#events">Outputs</a><a href="#api">Props API</a></div> <div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#layout-splitter-demo-1">Drag or arrow the divider</a><a href="#layout-splitter-demo-2">Stacked panes</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
</aside> </aside>
</div> </div>
<nav class="detail-pagination"> <nav class="detail-pagination">
@@ -14,6 +14,9 @@ page NavbarDetail {
state playground_maxWidth = ctx.url.searchParams.get("pg_maxWidth") ?? "full" state playground_maxWidth = ctx.url.searchParams.get("pg_maxWidth") ?? "full"
state playground_mobileLabel = ctx.url.searchParams.get("pg_mobileLabel") ?? "Navbar" state playground_mobileLabel = ctx.url.searchParams.get("pg_mobileLabel") ?? "Navbar"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1" state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
state demo_obj_0 = JSON.parse("{\"label\":\"Northwind\",\"description\":\"Console\",\"href\":\"/\"}")
state demo_obj_1 = JSON.parse("{\"label\":\"Northwind\",\"icon\":\"icon-[lucide--box]\",\"href\":\"/\"}")
state demo_obj_2 = JSON.parse("{\"label\":\"Acme\",\"href\":\"/\"}")
seo { seo {
title = "Navbar" title = "Navbar"
description = "Theme-aware, responsive navbar component." description = "Theme-aware, responsive navbar component."
@@ -600,289 +603,54 @@ page NavbarDetail {
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list"> <section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Recommended</span><h2>Production default</h2><p>Balanced spacing, hierarchy, and content for the most common product workflow.</p></div> <div><span class="demo-case-eyebrow">Recommended</span><h2>Brand, links and actions</h2><p>The everyday shape: a brand, a row of links with the current page marked, and calls to action on the right. Arrow keys move along the menu bar.</p></div>
<span class="demo-case-number">01</span> <span class="demo-case-number">01</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="navbar-demo-1" class="demo-canvas demo-canvas--1"> <div id="navbar-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Navbar size="default" label="Navbar" topbarLabel="Navbar" brand='({"id":"navbar-0-1","key":"navbar-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]})' items='[{"id":"navbar-0-1","key":"navbar-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"navbar-0-2","key":"navbar-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' actions='[{"id":"navbar-0-1","key":"navbar-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"navbar-0-2","key":"navbar-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' sticky="false" openOnHover="false" mobileLabel="Navbar" class="showcase-instance showcase-instance--1" /></div> <div class="demo-render"><Navbar size="default" label="Primary navigation" topbarLabel="Navbar" brand='{demo_obj_0}' items='[{"label":"Overview","href":"/","value":"overview"},{"label":"Projects","href":"/projects","value":"projects"},{"label":"Reports","href":"/reports","value":"reports"}]' actions='[{"label":"Sign in","href":"/login","variant":"link"},{"label":"Start free","href":"/signup","variant":"primary"}]' active="projects" sticky="false" openOnHover="false" mobileLabel="Navbar" class="showcase-instance showcase-instance--1" /></div>
</div> </div>
<section class="demo-code" aria-label="Production default usage"> <section class="demo-code" aria-label="Brand, links and actions usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Navbar <pre><code>&lt;Navbar
label="Navbar"
topbarLabel="Navbar" topbarLabel="Navbar"
brand='<span>&#123;</span> brand='<span>&#123;</span>
"id": "navbar-0-1", "label": "Northwind",
"key": "navbar-0-1", "description": "Console",
"value": "primary", "href": "/"
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>' <span>&#125;</span>'
items='[ items='[
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-0-1", "label": "Overview",
"key": "navbar-0-1", "href": "/",
"value": "primary", "value": "overview"
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>, <span>&#125;</span>,
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-0-2", "label": "Projects",
"key": "navbar-0-2", "href": "/projects",
"value": "secondary", "value": "projects"
"label": "Additional option", <span>&#125;</span>,
"title": "Supporting example", <span>&#123;</span>
"description": "A clean default configuration for everyday product interfaces.", "label": "Reports",
"text": "Another configurable message.", "href": "/reports",
"href": "#navbar-demo", "value": "reports"
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 32,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span> <span>&#125;</span>
]' ]'
actions='[ actions='[
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-0-1", "label": "Sign in",
"key": "navbar-0-1", "href": "/login",
"value": "primary", "variant": "link"
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>, <span>&#125;</span>,
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-0-2", "label": "Start free",
"key": "navbar-0-2", "href": "/signup",
"value": "secondary", "variant": "primary"
"label": "Additional option",
"title": "Supporting example",
"description": "A clean default configuration for everyday product interfaces.",
"text": "Another configurable message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 32,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span> <span>&#125;</span>
]' ]'
active="projects"
mobileLabel="Navbar"&gt; mobileLabel="Navbar"&gt;
&lt;div data-slot="topbar"&gt; &lt;div data-slot="topbar"&gt;
&lt;div class="showcase-slot"&gt;topbar slot&lt;/div&gt; &lt;div class="showcase-slot"&gt;topbar slot&lt;/div&gt;
@@ -896,290 +664,86 @@ page NavbarDetail {
</article> </article>
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Dense UI</span><h2>Compact application</h2><p>A tighter variation for dashboards, side panels, tables, and operational interfaces.</p></div> <div><span class="demo-case-eyebrow">Advanced</span><h2>Dropdown and mega panel</h2><p>An item carrying children opens a panel built from native details and summary, so it is keyboard operable without any extra script. Give a child its own children to get a titled column inside the panel.</p></div>
<span class="demo-case-number">02</span> <span class="demo-case-number">02</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="navbar-demo-2" class="demo-canvas demo-canvas--2"> <div id="navbar-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Navbar size="sm" label="Compact example" topbarLabel="Compact example" brand='({"id":"navbar-1-1","key":"navbar-1-1","value":"primary","label":"Secondary workflow","title":"Secondary workflow","description":"A compact configuration designed for dense application layouts.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":24,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]})' items='[{"id":"navbar-1-1","key":"navbar-1-1","value":"primary","label":"Secondary workflow","title":"Secondary workflow","description":"A compact configuration designed for dense application layouts.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":24,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"navbar-1-2","key":"navbar-1-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A compact configuration designed for dense application layouts.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":48,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' actions='[{"id":"navbar-1-1","key":"navbar-1-1","value":"primary","label":"Secondary workflow","title":"Secondary workflow","description":"A compact configuration designed for dense application layouts.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":24,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"navbar-1-2","key":"navbar-1-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A compact configuration designed for dense application layouts.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":48,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' sticky="false" openOnHover="false" mobileLabel="Compact example" class="showcase-instance showcase-instance--2" /></div> <div class="demo-render"><Navbar size="sm" label="Compact example" topbarLabel="Compact example" brand='{demo_obj_1}' items='[{"label":"Overview","href":"/","value":"overview"},{"label":"Products","value":"products","columns":2,"description":"Everything in the platform.","children":[{"label":"Platform","children":[{"label":"Runtime","href":"/runtime","description":"The browser half.","icon":"icon-[lucide--cpu]"},{"label":"Compiler","href":"/compiler","description":"WRN to JavaScript.","icon":"icon-[lucide--hammer]"}]},{"label":"Tooling","children":[{"label":"CLI","href":"/cli","description":"Scaffold and deploy."},{"label":"Editor","href":"/editor","description":"Language server."}]}]},{"label":"Pricing","href":"/pricing","value":"pricing"}]' actions='[{"label":"Book a demo","href":"/demo","variant":"primary"}]' active="overview" sticky="false" openOnHover="false" mobileLabel="Compact example" class="showcase-instance showcase-instance--2" /></div>
</div> </div>
<section class="demo-code" aria-label="Compact application usage"> <section class="demo-code" aria-label="Dropdown and mega panel usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Navbar <pre><code>&lt;Navbar
size="sm" size="sm"
label="Compact example" label="Compact example"
topbarLabel="Compact example" topbarLabel="Compact example"
brand='<span>&#123;</span> brand='<span>&#123;</span>
"id": "navbar-1-1", "label": "Northwind",
"key": "navbar-1-1", "icon": "icon-[lucide--box]",
"value": "primary", "href": "/"
"label": "Secondary workflow",
"title": "Secondary workflow",
"description": "A compact configuration designed for dense application layouts.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 24,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>' <span>&#125;</span>'
items='[ items='[
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-1-1", "label": "Overview",
"key": "navbar-1-1", "href": "/",
"value": "primary", "value": "overview"
"label": "Secondary workflow", <span>&#125;</span>,
"title": "Secondary workflow", <span>&#123;</span>
"description": "A compact configuration designed for dense application layouts.", "label": "Products",
"text": "A configurable sample message.", "value": "products",
"href": "#navbar-demo", "columns": 2,
"actionHref": "#navbar-demo", "description": "Everything in the platform.",
"actionLabel": "View details", "children": [
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 24,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 1",
"items": [
<span>&#123;</span> <span>&#123;</span>
"label": "Nested option A", "label": "Platform",
"value": "nested-a" "children": [
<span>&#123;</span>
"label": "Runtime",
"href": "/runtime",
"description": "The browser half.",
"icon": "icon-[lucide--cpu]"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Compiler",
"href": "/compiler",
"description": "WRN to JavaScript.",
"icon": "icon-[lucide--hammer]"
<span>&#125;</span>
]
<span>&#125;</span>, <span>&#125;</span>,
<span>&#123;</span> <span>&#123;</span>
"label": "Nested option B", "label": "Tooling",
"value": "nested-b" "children": [
<span>&#123;</span>
"label": "CLI",
"href": "/cli",
"description": "Scaffold and deploy."
<span>&#125;</span>,
<span>&#123;</span>
"label": "Editor",
"href": "/editor",
"description": "Language server."
<span>&#125;</span>
]
<span>&#125;</span> <span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
] ]
<span>&#125;</span>, <span>&#125;</span>,
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-1-2", "label": "Pricing",
"key": "navbar-1-2", "href": "/pricing",
"value": "secondary", "value": "pricing"
"label": "Additional option",
"title": "Supporting example",
"description": "A compact configuration designed for dense application layouts.",
"text": "Another configurable message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 48,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span> <span>&#125;</span>
]' ]'
actions='[ actions='[
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-1-1", "label": "Book a demo",
"key": "navbar-1-1", "href": "/demo",
"value": "primary", "variant": "primary"
"label": "Secondary workflow",
"title": "Secondary workflow",
"description": "A compact configuration designed for dense application layouts.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 24,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>,
<span>&#123;</span>
"id": "navbar-1-2",
"key": "navbar-1-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A compact configuration designed for dense application layouts.",
"text": "Another configurable message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 48,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span> <span>&#125;</span>
]' ]'
active="overview"
mobileLabel="Compact example"&gt; mobileLabel="Compact example"&gt;
&lt;div data-slot="topbar"&gt; &lt;div data-slot="topbar"&gt;
&lt;div class="showcase-slot"&gt;topbar slot&lt;/div&gt; &lt;div class="showcase-slot"&gt;topbar slot&lt;/div&gt;
@@ -1193,291 +757,46 @@ page NavbarDetail {
</article> </article>
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Extended</span><h2>Rich configuration</h2><p>A more expressive variation using additional data, stronger emphasis, and optional states.</p></div> <div><span class="demo-case-eyebrow">Compact</span><h2>Sticky and compact</h2><p>Sticky keeps the bar pinned while the page scrolls. The toggle appears below the tablet breakpoint and the links collapse behind it.</p></div>
<span class="demo-case-number">03</span> <span class="demo-case-number">03</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="navbar-demo-3" class="demo-canvas demo-canvas--3"> <div id="navbar-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Navbar size="lg" label="Advanced example" topbarLabel="Advanced example" brand='({"id":"navbar-2-1","key":"navbar-2-1","value":"primary","label":"Advanced workflow","title":"Advanced workflow","description":"A richer configuration with more supporting information and actions.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":32,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]})' items='[{"id":"navbar-2-1","key":"navbar-2-1","value":"primary","label":"Advanced workflow","title":"Advanced workflow","description":"A richer configuration with more supporting information and actions.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":32,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]},{"id":"navbar-2-2","key":"navbar-2-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A richer configuration with more supporting information and actions.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":false,"selected":false,"checked":false,"disabled":true,"outgoing":true,"open":true,"number":2,"count":64,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]}]' actions='[{"id":"navbar-2-1","key":"navbar-2-1","value":"primary","label":"Advanced workflow","title":"Advanced workflow","description":"A richer configuration with more supporting information and actions.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":32,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]},{"id":"navbar-2-2","key":"navbar-2-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A richer configuration with more supporting information and actions.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":false,"selected":false,"checked":false,"disabled":true,"outgoing":true,"open":true,"number":2,"count":64,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]}]' sticky="false" openOnHover="false" mobileLabel="Advanced example" class="showcase-instance showcase-instance--3" /></div> <div class="demo-render"><Navbar size="sm" label="Advanced example" topbarLabel="Advanced example" brand='{demo_obj_2}' items='[{"label":"Docs","href":"/docs","value":"docs"},{"label":"Support","href":"/support","value":"support"}]' actions='[{"label":"Dashboard","href":"/app","variant":"primary"}]' active="docs" sticky="true" openOnHover="false" mobileLabel="Toggle navigation" class="showcase-instance showcase-instance--3" /></div>
</div> </div>
<section class="demo-code" aria-label="Rich configuration usage"> <section class="demo-code" aria-label="Sticky and compact usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Navbar <pre><code>&lt;Navbar
size="lg" size="sm"
label="Advanced example" label="Advanced example"
topbarLabel="Advanced example" topbarLabel="Advanced example"
brand='<span>&#123;</span> brand='<span>&#123;</span>
"id": "navbar-2-1", "label": "Acme",
"key": "navbar-2-1", "href": "/"
"value": "primary",
"label": "Advanced workflow",
"title": "Advanced workflow",
"description": "A richer configuration with more supporting information and actions.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 32,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
<span>&#125;</span>' <span>&#125;</span>'
items='[ items='[
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-2-1", "label": "Docs",
"key": "navbar-2-1", "href": "/docs",
"value": "primary", "value": "docs"
"label": "Advanced workflow",
"title": "Advanced workflow",
"description": "A richer configuration with more supporting information and actions.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 32,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
<span>&#125;</span>, <span>&#125;</span>,
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-2-2", "label": "Support",
"key": "navbar-2-2", "href": "/support",
"value": "secondary", "value": "support"
"label": "Additional option",
"title": "Supporting example",
"description": "A richer configuration with more supporting information and actions.",
"text": "Another configurable message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": false,
"selected": false,
"checked": false,
"disabled": true,
"outgoing": true,
"open": true,
"number": 2,
"count": 64,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
<span>&#125;</span> <span>&#125;</span>
]' ]'
actions='[ actions='[
<span>&#123;</span> <span>&#123;</span>
"id": "navbar-2-1", "label": "Dashboard",
"key": "navbar-2-1", "href": "/app",
"value": "primary", "variant": "primary"
"label": "Advanced workflow",
"title": "Advanced workflow",
"description": "A richer configuration with more supporting information and actions.",
"text": "A configurable sample message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 32,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
<span>&#125;</span>,
<span>&#123;</span>
"id": "navbar-2-2",
"key": "navbar-2-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A richer configuration with more supporting information and actions.",
"text": "Another configurable message.",
"href": "#navbar-demo",
"actionHref": "#navbar-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": false,
"selected": false,
"checked": false,
"disabled": true,
"outgoing": true,
"open": true,
"number": 2,
"count": 64,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
<span>&#125;</span> <span>&#125;</span>
]' ]'
mobileLabel="Advanced example"&gt; active="docs"
sticky="true"&gt;
&lt;div data-slot="topbar"&gt; &lt;div data-slot="topbar"&gt;
&lt;div class="showcase-slot"&gt;topbar slot&lt;/div&gt; &lt;div class="showcase-slot"&gt;topbar slot&lt;/div&gt;
&lt;/div&gt; &lt;/div&gt;
@@ -1520,7 +839,7 @@ if (component) registerOutputHandler(component, "action", (payload) =&gt; <span>
<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>"Primary navigation"</code></td><td>No</td></tr><tr><td><code>topbarLabel</code></td><td>string</td><td><code>"Utility navigation"</code></td><td>No</td></tr><tr><td><code>brand</code></td><td>Record&lt;string, unknown&gt;</td><td><code><span>&#123;</span><span>&#125;</span></code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>actions</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>sticky</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>openOnHover</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>maxWidth</code></td><td>string</td><td><code>"full"</code></td><td>No</td></tr><tr><td><code>mobileLabel</code></td><td>string</td><td><code>"Toggle navigation"</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>"Primary navigation"</code></td><td>No</td></tr><tr><td><code>topbarLabel</code></td><td>string</td><td><code>"Utility navigation"</code></td><td>No</td></tr><tr><td><code>brand</code></td><td>Record&lt;string, unknown&gt;</td><td><code><span>&#123;</span><span>&#125;</span></code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>actions</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>sticky</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>openOnHover</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>maxWidth</code></td><td>string</td><td><code>"full"</code></td><td>No</td></tr><tr><td><code>mobileLabel</code></td><td>string</td><td><code>"Toggle navigation"</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> </main>
<aside class="detail-aside"> <aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#navbar-demo-1">Production default</a><a href="#navbar-demo-2">Compact application</a><a href="#navbar-demo-3">Rich configuration</a><a href="#events">Outputs</a><a href="#api">Props API</a></div> <div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#navbar-demo-1">Brand, links and actions</a><a href="#navbar-demo-2">Dropdown and mega panel</a><a href="#navbar-demo-3">Sticky and compact</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
</aside> </aside>
</div> </div>
<nav class="detail-pagination"> <nav class="detail-pagination">
@@ -266,16 +266,16 @@ page ScrollspyDetail {
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list"> <section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Recommended</span><h2>Table of contents</h2><p>Each href points at an element on the page. The runtime observes those elements and moves aria-current to the link for whichever one is in view, so the marker follows the reader without any component state.</p></div> <div><span class="demo-case-eyebrow">Recommended</span><h2>Follows the reader</h2><p>Scroll the page and the marker moves to whichever section is in view. The sections below are real page content, because the runtime observes against the viewport -- a table of contents inside a small scrolling box would have nothing to react to.</p></div>
<span class="demo-case-number">01</span> <span class="demo-case-number">01</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="scrollspy-demo-1" class="demo-canvas demo-canvas--1"> <div id="scrollspy-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Scrollspy size="default" items='[{"label":"Overview","href":"#overview"},{"label":"Installation","href":"#installation"},{"label":"Usage","href":"#usage"},{"label":"API","href":"#api"}]' active="#overview" label="Scrollspy" heading="On this page" class="showcase-instance showcase-instance--1" /></div> <div class="demo-render"><div class="showcase-spy-doc"><section id="overview" class="showcase-spy-section"><h3>Overview</h3><p>Scroll this page and watch the marker in the list move. The runtime observes these sections against the viewport, so the current link follows whatever you are reading.</p></section><section id="installation" class="showcase-spy-section"><h3>Installation</h3><p>Each link points at one of these ids. Nothing is wired up by hand -- the component only renders the links, and the runtime moves aria-current.</p></section><section id="usage" class="showcase-spy-section"><h3>Usage</h3><p>The active link is marked with aria-current="location", which is what a screen reader announces, and styled through data-active.</p></section><section id="api" class="showcase-spy-section"><h3>API</h3><p>Clicking a link jumps to its section and marks it immediately, so the control responds even before the scroll settles.</p></section></div><Scrollspy size="default" items='[{"label":"Overview","href":"#overview"},{"label":"Installation","href":"#installation"},{"label":"Usage","href":"#usage"},{"label":"API","href":"#api"}]' active="#overview" label="Scrollspy" heading="On this page" class="showcase-instance showcase-instance--1" /></div>
</div> </div>
<section class="demo-code" aria-label="Table of contents usage"> <section class="demo-code" aria-label="Follows the reader usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Scrollspy <pre><code>&lt;Scrollspy
items='[ items='[
@@ -312,7 +312,7 @@ page ScrollspyDetail {
<div id="scrollspy-demo-2" class="demo-canvas demo-canvas--2"> <div id="scrollspy-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Scrollspy size="sm" items='[{"label":"Getting started","href":"#getting-started"},{"label":"Configuration","href":"#configuration"},{"label":"Troubleshooting","href":"#troubleshooting"}]' active="#configuration" label="Compact example" heading="Sections" class="showcase-instance showcase-instance--2" /></div> <div class="demo-render"><Scrollspy size="sm" items='[{"label":"Overview","href":"#overview"},{"label":"Installation","href":"#installation"},{"label":"Usage","href":"#usage"}]' active="#installation" label="Compact example" heading="Sections" class="showcase-instance showcase-instance--2" /></div>
</div> </div>
<section class="demo-code" aria-label="Compact rail usage"> <section class="demo-code" aria-label="Compact rail usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -320,19 +320,19 @@ page ScrollspyDetail {
size="sm" size="sm"
items='[ items='[
<span>&#123;</span> <span>&#123;</span>
"label": "Getting started", "label": "Overview",
"href": "#getting-started" "href": "#overview"
<span>&#125;</span>, <span>&#125;</span>,
<span>&#123;</span> <span>&#123;</span>
"label": "Configuration", "label": "Installation",
"href": "#configuration" "href": "#installation"
<span>&#125;</span>, <span>&#125;</span>,
<span>&#123;</span> <span>&#123;</span>
"label": "Troubleshooting", "label": "Usage",
"href": "#troubleshooting" "href": "#usage"
<span>&#125;</span> <span>&#125;</span>
]' ]'
active="#configuration" active="#installation"
label="Compact example" label="Compact example"
heading="Sections" heading="Sections"
/&gt;</code></pre> /&gt;</code></pre>
@@ -351,7 +351,7 @@ if (component) registerOutputHandler(component, "change", (payload) =&gt; <span>
<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>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"On this page"</code></td><td>No</td></tr><tr><td><code>heading</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> <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>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"On this page"</code></td><td>No</td></tr><tr><td><code>heading</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> </main>
<aside class="detail-aside"> <aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#scrollspy-demo-1">Table of contents</a><a href="#scrollspy-demo-2">Compact rail</a><a href="#events">Outputs</a><a href="#api">Props API</a></div> <div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#scrollspy-demo-1">Follows the reader</a><a href="#scrollspy-demo-2">Compact rail</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
</aside> </aside>
</div> </div>
<nav class="detail-pagination"> <nav class="detail-pagination">
@@ -8,6 +8,14 @@ page StepperDetail {
state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal" state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal"
state playground_clickable = (ctx.url.searchParams.get("pg_clickable") ?? "false") === "true" state playground_clickable = (ctx.url.searchParams.get("pg_clickable") ?? "false") === "true"
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Stepper" state playground_label = ctx.url.searchParams.get("pg_label") ?? "Stepper"
state playground_showPanel = (ctx.url.searchParams.get("pg_showPanel") ?? "true") === "true"
state playground_controls = (ctx.url.searchParams.get("pg_controls") ?? "false") === "true"
state playground_allowSkip = (ctx.url.searchParams.get("pg_allowSkip") ?? "false") === "true"
state playground_nextDisabled = (ctx.url.searchParams.get("pg_nextDisabled") ?? "false") === "true"
state playground_backLabel = ctx.url.searchParams.get("pg_backLabel") ?? "Stepper"
state playground_nextLabel = ctx.url.searchParams.get("pg_nextLabel") ?? "Stepper"
state playground_skipLabel = ctx.url.searchParams.get("pg_skipLabel") ?? "Stepper"
state playground_finishLabel = ctx.url.searchParams.get("pg_finishLabel") ?? "Stepper"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1" state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo { seo {
title = "Stepper" title = "Stepper"
@@ -22,16 +30,16 @@ page StepperDetail {
<span class="showcase-eyebrow">Navigation component</span> <span class="showcase-eyebrow">Navigation component</span>
<h1>Stepper</h1> <h1>Stepper</h1>
<p>Theme-aware, responsive stepper component.</p> <p>Theme-aware, responsive stepper component.</p>
<div class="detail-badges"><span>8 props</span><span>2 slots</span><span>1 outputs</span><span>Theme ready</span><span>Responsive</span></div> <div class="detail-badges"><span>16 props</span><span>3 slots</span><span>5 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div> </div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div> <div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header> </header>
<section id="playground" class="detail-section playground-section" data-playground data-playground-component="Stepper" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="change"> <section id="playground" class="detail-section playground-section" data-playground data-playground-component="Stepper" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="change,back,next,skip,finish">
<div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Stepper</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div> <div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Stepper</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div>
<div class="playground-workbench"> <div class="playground-workbench">
<div class="playground-preview"> <div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div> <div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Stepper color='{playground_color}' size='{playground_size}' steps='{playground_steps}' active='{playground_active}' orientation='{playground_orientation}' clickable='{playground_clickable}' label='{playground_label}' class='{playground_class}' /></div> <div class="playground-preview-source" data-playground-preview><Stepper color='{playground_color}' size='{playground_size}' steps='{playground_steps}' active='{playground_active}' orientation='{playground_orientation}' clickable='{playground_clickable}' label='{playground_label}' showPanel='{playground_showPanel}' controls='{playground_controls}' allowSkip='{playground_allowSkip}' nextDisabled='{playground_nextDisabled}' backLabel='{playground_backLabel}' nextLabel='{playground_nextLabel}' skipLabel='{playground_skipLabel}' finishLabel='{playground_finishLabel}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code"> <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> <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;Stepper <pre><code data-playground-code>&lt;Stepper
@@ -143,17 +151,25 @@ page StepperDetail {
] ]
<span>&#125;</span> <span>&#125;</span>
]' ]'
label="Stepper"&gt; label="Stepper"
showPanel="true"
backLabel="Stepper"
nextLabel="Stepper"
skipLabel="Stepper"
finishLabel="Stepper"&gt;
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt; &lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt; &lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div data-slot="panel-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;panel-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre> &lt;/Stepper&gt;</code></pre>
</section> </section>
<div class="playground-status" data-playground-status aria-live="polite"></div> <div class="playground-status" data-playground-status aria-live="polite"></div>
<div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div> <div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div>
</div> </div>
<form class="playground-controls" data-playground-form> <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> <header><div><span>Component props</span><strong>16 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-stepper-color"><span><strong>color</strong><small>string</small></span><select id="playground-stepper-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-stepper-size"><span><strong>size</strong><small>string</small></span><select id="playground-stepper-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-stepper-steps"><span><strong>steps</strong><small>unknown[]</small></span><textarea id="playground-stepper-steps" name="pg_steps" rows="5" spellcheck="false" data-playground-json>[ <div class="playground-fields"><label class="playground-field" for="playground-stepper-color"><span><strong>color</strong><small>string</small></span><select id="playground-stepper-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-stepper-size"><span><strong>size</strong><small>string</small></span><select id="playground-stepper-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-stepper-steps"><span><strong>steps</strong><small>unknown[]</small></span><textarea id="playground-stepper-steps" name="pg_steps" rows="5" spellcheck="false" data-playground-json>[
&#123; &#123;
"id": "stepper-0-1", "id": "stepper-0-1",
@@ -261,7 +277,7 @@ page StepperDetail {
"Standard" "Standard"
] ]
&#125; &#125;
]</textarea><em data-playground-error></em></label><label class="playground-field" for="playground-stepper-active"><span><strong>active</strong><small>number</small></span><input id="playground-stepper-active" name="pg_active" type="number" value="0" /></label><label class="playground-field" for="playground-stepper-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-stepper-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-toggle" for="playground-stepper-clickable"><span><strong>clickable</strong><small>boolean</small></span><input id="playground-stepper-clickable" name="pg_clickable" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-stepper-label"><span><strong>label</strong><small>string</small></span><input id="playground-stepper-label" name="pg_label" type="text" value="Stepper" /></label><label class="playground-field" for="playground-stepper-class"><span><strong>class</strong><small>string</small></span><input id="playground-stepper-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div> ]</textarea><em data-playground-error></em></label><label class="playground-field" for="playground-stepper-active"><span><strong>active</strong><small>number</small></span><input id="playground-stepper-active" name="pg_active" type="number" value="0" /></label><label class="playground-field" for="playground-stepper-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-stepper-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-toggle" for="playground-stepper-clickable"><span><strong>clickable</strong><small>boolean</small></span><input id="playground-stepper-clickable" name="pg_clickable" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-stepper-label"><span><strong>label</strong><small>string</small></span><input id="playground-stepper-label" name="pg_label" type="text" value="Stepper" /></label><label class="playground-toggle" for="playground-stepper-showPanel"><span><strong>show Panel</strong><small>boolean</small></span><input id="playground-stepper-showPanel" name="pg_showPanel" type="checkbox" value="true" checked data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-toggle" for="playground-stepper-controls"><span><strong>controls</strong><small>boolean</small></span><input id="playground-stepper-controls" name="pg_controls" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-toggle" for="playground-stepper-allowSkip"><span><strong>allow Skip</strong><small>boolean</small></span><input id="playground-stepper-allowSkip" name="pg_allowSkip" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-toggle" for="playground-stepper-nextDisabled"><span><strong>next Disabled</strong><small>boolean</small></span><input id="playground-stepper-nextDisabled" name="pg_nextDisabled" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-stepper-backLabel"><span><strong>back Label</strong><small>string</small></span><input id="playground-stepper-backLabel" name="pg_backLabel" type="text" value="Stepper" /></label><label class="playground-field" for="playground-stepper-nextLabel"><span><strong>next Label</strong><small>string</small></span><input id="playground-stepper-nextLabel" name="pg_nextLabel" type="text" value="Stepper" /></label><label class="playground-field" for="playground-stepper-skipLabel"><span><strong>skip Label</strong><small>string</small></span><input id="playground-stepper-skipLabel" name="pg_skipLabel" type="text" value="Stepper" /></label><label class="playground-field" for="playground-stepper-finishLabel"><span><strong>finish Label</strong><small>string</small></span><input id="playground-stepper-finishLabel" name="pg_finishLabel" type="text" value="Stepper" /></label><label class="playground-field" for="playground-stepper-class"><span><strong>class</strong><small>string</small></span><input id="playground-stepper-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form> </form>
</div> </div>
</section> </section>
@@ -277,7 +293,7 @@ page StepperDetail {
<div id="stepper-demo-1" class="demo-canvas demo-canvas--1"> <div id="stepper-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Stepper size="default" steps='[{"label":"Account","description":"Your details"},{"label":"Billing","description":"Payment method"},{"label":"Confirm","description":"Review and submit"}]' active="1" clickable="false" label="Stepper" class="showcase-instance showcase-instance--1" /></div> <div class="demo-render"><Stepper size="default" steps='[{"label":"Account","description":"Your details"},{"label":"Billing","description":"Payment method"},{"label":"Confirm","description":"Review and submit"}]' active="1" clickable="false" label="Stepper" showPanel="false" controls="false" allowSkip="false" nextDisabled="false" backLabel="Back" nextLabel="Next" skipLabel="Skip" finishLabel="Finish" class="showcase-instance showcase-instance--1" /></div>
</div> </div>
<section class="demo-code" aria-label="Horizontal progress usage"> <section class="demo-code" aria-label="Horizontal progress usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -301,6 +317,9 @@ page StepperDetail {
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt; &lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt; &lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div data-slot="panel-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;panel-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre> &lt;/Stepper&gt;</code></pre>
</section> </section>
</div> </div>
@@ -314,7 +333,7 @@ page StepperDetail {
<div id="stepper-demo-2" class="demo-canvas demo-canvas--2"> <div id="stepper-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-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Stepper size="sm" steps='[{"label":"Cloned","icon":"icon-[lucide--git-branch]"},{"label":"Built","icon":"icon-[lucide--hammer]"},{"label":"Deployed","icon":"icon-[lucide--rocket]"}]' active="2" orientation="vertical" clickable="false" label="Compact example" class="showcase-instance showcase-instance--2" /></div> <div class="demo-render"><Stepper size="sm" steps='[{"label":"Cloned","icon":"icon-[lucide--git-branch]"},{"label":"Built","icon":"icon-[lucide--hammer]"},{"label":"Deployed","icon":"icon-[lucide--rocket]"}]' active="2" orientation="vertical" clickable="false" label="Compact example" showPanel="false" controls="false" allowSkip="false" nextDisabled="false" backLabel="Back" nextLabel="Next" skipLabel="Skip" finishLabel="Finish" class="showcase-instance showcase-instance--2" /></div>
</div> </div>
<section class="demo-code" aria-label="Vertical with icons usage"> <section class="demo-code" aria-label="Vertical with icons usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -340,6 +359,97 @@ page StepperDetail {
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt; &lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt; &lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div data-slot="panel-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;panel-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Advanced</span><h2>Wizard with content and controls</h2><p>showPanel renders each step body and shows only the active one, the same contract Tabs uses. controls adds Back, Skip and Next, which becomes Finish on the last step.</p></div>
<span class="demo-case-number">03</span>
</header>
<div class="demo-workbench">
<div id="stepper-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"><Stepper size="lg" steps='[{"label":"Account","title":"Your details","content":"Name, email and a password."},{"label":"Billing","title":"How you pay","content":"Card or invoice."},{"label":"Confirm","title":"Review","content":"Check everything before submitting."}]' active="1" clickable="false" label="Advanced example" showPanel="true" controls="true" allowSkip="true" nextDisabled="false" backLabel="Back" nextLabel="Next" skipLabel="Skip" finishLabel="Finish" class="showcase-instance showcase-instance--3" /></div>
</div>
<section class="demo-code" aria-label="Wizard with content and controls 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;Stepper
size="lg"
steps='[
<span>&#123;</span>
"label": "Account",
"title": "Your details",
"content": "Name, email and a password."
<span>&#125;</span>,
<span>&#123;</span>
"label": "Billing",
"title": "How you pay",
"content": "Card or invoice."
<span>&#125;</span>,
<span>&#123;</span>
"label": "Confirm",
"title": "Review",
"content": "Check everything before submitting."
<span>&#125;</span>
]'
active="1"
label="Advanced example"
showPanel="true"
controls="true"
allowSkip="true"&gt;
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;div data-slot="panel-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;panel-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Advanced</span><h2>Held until the step is valid</h2><p>The same wizard with nextDisabled set. The component never validates anything itself: the page owns the form, sets this while the step is incomplete, and clears it once the step passes.</p></div>
<span class="demo-case-number">04</span>
</header>
<div class="demo-workbench">
<div id="stepper-demo-4" class="demo-canvas demo-canvas--4">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Stepper size="lg" steps='[{"label":"Account","title":"Your details","content":"This step is not complete yet."},{"label":"Billing","title":"How you pay","content":"Card or invoice."}]' active="0" clickable="false" label="Advanced example" showPanel="true" controls="true" allowSkip="false" nextDisabled="true" backLabel="Back" nextLabel="Next" skipLabel="Skip" finishLabel="Finish" class="showcase-instance showcase-instance--4" /></div>
</div>
<section class="demo-code" aria-label="Held until the step is valid 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;Stepper
size="lg"
steps='[
<span>&#123;</span>
"label": "Account",
"title": "Your details",
"content": "This step is not complete yet."
<span>&#125;</span>,
<span>&#123;</span>
"label": "Billing",
"title": "How you pay",
"content": "Card or invoice."
<span>&#125;</span>
]'
label="Advanced example"
showPanel="true"
controls="true"
nextDisabled="true"&gt;
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;div data-slot="panel-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;panel-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre> &lt;/Stepper&gt;</code></pre>
</section> </section>
</div> </div>
@@ -347,13 +457,13 @@ page StepperDetail {
<article class="demo-case"> <article class="demo-case">
<header class="demo-case-header"> <header class="demo-case-header">
<div><span class="demo-case-eyebrow">Advanced</span><h2>Clickable steps</h2><p>With clickable the steps emit a change output and take arrow-key roving focus. Without it the stepper is read-only and stays out of the tab order.</p></div> <div><span class="demo-case-eyebrow">Advanced</span><h2>Clickable steps</h2><p>With clickable the steps emit a change output and take arrow-key roving focus. Without it the stepper is read-only and stays out of the tab order.</p></div>
<span class="demo-case-number">03</span> <span class="demo-case-number">05</span>
</header> </header>
<div class="demo-workbench"> <div class="demo-workbench">
<div id="stepper-demo-3" class="demo-canvas demo-canvas--3"> <div id="stepper-demo-5" class="demo-canvas demo-canvas--5">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div> <div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Stepper size="lg" steps='[{"label":"One"},{"label":"Two"},{"label":"Three"}]' active="0" clickable="true" label="Advanced example" class="showcase-instance showcase-instance--3" /></div> <div class="demo-render"><Stepper size="lg" steps='[{"label":"One"},{"label":"Two"},{"label":"Three"}]' active="0" clickable="true" label="Advanced example" showPanel="false" controls="false" allowSkip="false" nextDisabled="false" backLabel="Back" nextLabel="Next" skipLabel="Skip" finishLabel="Finish" class="showcase-instance showcase-instance--5" /></div>
</div> </div>
<section class="demo-code" aria-label="Clickable steps usage"> <section class="demo-code" aria-label="Clickable steps usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header> <header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
@@ -375,23 +485,46 @@ page StepperDetail {
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt; &lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt; &lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div data-slot="panel-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;panel-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre> &lt;/Stepper&gt;</code></pre>
</section> </section>
</div> </div>
</article></div></section> </article></div></section>
<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@change</code><span>Fires when the component's committed value or selection changes.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;Stepper <section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@change</code><span>Fires when the component's committed value or selection changes.</span></div><div><code>@back</code><span>Fires when the component emits the back event.</span></div><div><code>@next</code><span>Fires when the component emits the next event.</span></div><div><code>@skip</code><span>Fires when the component emits the skip event.</span></div><div><code>@finish</code><span>Fires when the component emits the finish event.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;Stepper
@change='console.log(payload)' @change='console.log(payload)'
@back='console.log(payload)'
@next='console.log(payload)'
@skip='console.log(payload)'
@finish='console.log(payload)'
/&gt;</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Direct output handlers</span><small>.js</small></header><pre><code>import <span>&#123;</span> registerOutputHandler <span>&#125;</span> from "@wrnexus/csr/outputs" /&gt;</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Direct output handlers</span><small>.js</small></header><pre><code>import <span>&#123;</span> registerOutputHandler <span>&#125;</span> from "@wrnexus/csr/outputs"
const component = document.querySelector("[data-ui-component=\"Stepper\"], [data-component=\"Stepper\"]") const component = document.querySelector("[data-ui-component=\"Stepper\"], [data-component=\"Stepper\"]")
if (component) registerOutputHandler(component, "change", (payload) =&gt; <span>&#123;</span> if (component) registerOutputHandler(component, "change", (payload) =&gt; <span>&#123;</span>
console.log("change", payload) console.log("change", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "back", (payload) =&gt; <span>&#123;</span>
console.log("back", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "next", (payload) =&gt; <span>&#123;</span>
console.log("next", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "skip", (payload) =&gt; <span>&#123;</span>
console.log("skip", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "finish", (payload) =&gt; <span>&#123;</span>
console.log("finish", payload)
<span>&#125;</span>)</code></pre></section></div></div></section> <span>&#125;</span>)</code></pre></section></div></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>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>steps</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>number</td><td><code>0</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>clickable</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Progress"</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>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>steps</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>number</td><td><code>0</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>clickable</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Progress"</code></td><td>No</td></tr><tr><td><code>showPanel</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>controls</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>allowSkip</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>nextDisabled</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>backLabel</code></td><td>string</td><td><code>"Back"</code></td><td>No</td></tr><tr><td><code>nextLabel</code></td><td>string</td><td><code>"Next"</code></td><td>No</td></tr><tr><td><code>skipLabel</code></td><td>string</td><td><code>"Skip"</code></td><td>No</td></tr><tr><td><code>finishLabel</code></td><td>string</td><td><code>"Finish"</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> </main>
<aside class="detail-aside"> <aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#stepper-demo-1">Horizontal progress</a><a href="#stepper-demo-2">Vertical with icons</a><a href="#stepper-demo-3">Clickable steps</a><a href="#events">Outputs</a><a href="#api">Props API</a></div> <div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#stepper-demo-1">Horizontal progress</a><a href="#stepper-demo-2">Vertical with icons</a><a href="#stepper-demo-3">Wizard with content and controls</a><a href="#stepper-demo-4">Held until the step is valid</a><a href="#stepper-demo-5">Clickable steps</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
</aside> </aside>
</div> </div>
<nav class="detail-pagination"> <nav class="detail-pagination">
@@ -21,7 +21,7 @@ page ComponentShowcase {
<div class="showcase-stat"><strong>0</strong><span>Duplicate implementations</span></div> <div class="showcase-stat"><strong>0</strong><span>Duplicate implementations</span></div>
<div class="showcase-stat"><strong>11</strong><span>Focused categories</span></div> <div class="showcase-stat"><strong>11</strong><span>Focused categories</span></div>
</section> </section>
<section class="home-categories"><div class="home-section-heading"><span>Explore the system</span><h2>Everything your product needs</h2></div><div class="home-category-grid"><a class="home-category home-category--1" href="/advanced-forms"><span class="home-category-index">01</span><div><strong>Advanced Forms</strong><p>8 components · 112 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--2" href="/base"><span class="home-category-index">02</span><div><strong>Base</strong><p>27 components · 86 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--3" href="/core"><span class="home-category-index">03</span><div><strong>Core</strong><p>5 components · 13 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--4" href="/data"><span class="home-category-index">04</span><div><strong>Data</strong><p>3 components · 9 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--1" href="/forms"><span class="home-category-index">05</span><div><strong>Forms</strong><p>12 components · 36 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--2" href="/integrations"><span class="home-category-index">06</span><div><strong>Integrations</strong><p>11 components · 33 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--3" href="/layout"><span class="home-category-index">07</span><div><strong>Layout</strong><p>15 components · 45 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--4" href="/marketing"><span class="home-category-index">08</span><div><strong>Marketing</strong><p>10 components · 30 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--1" href="/navigation"><span class="home-category-index">09</span><div><strong>Navigation</strong><p>10 components · 28 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--2" href="/overlays"><span class="home-category-index">010</span><div><strong>Overlays</strong><p>6 components · 18 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--3" href="/tables"><span class="home-category-index">011</span><div><strong>Tables</strong><p>1 components · 9 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a></div></section> <section class="home-categories"><div class="home-section-heading"><span>Explore the system</span><h2>Everything your product needs</h2></div><div class="home-category-grid"><a class="home-category home-category--1" href="/advanced-forms"><span class="home-category-index">01</span><div><strong>Advanced Forms</strong><p>8 components · 112 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--2" href="/base"><span class="home-category-index">02</span><div><strong>Base</strong><p>27 components · 86 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--3" href="/core"><span class="home-category-index">03</span><div><strong>Core</strong><p>5 components · 13 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--4" href="/data"><span class="home-category-index">04</span><div><strong>Data</strong><p>3 components · 9 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--1" href="/forms"><span class="home-category-index">05</span><div><strong>Forms</strong><p>12 components · 36 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--2" href="/integrations"><span class="home-category-index">06</span><div><strong>Integrations</strong><p>11 components · 33 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--3" href="/layout"><span class="home-category-index">07</span><div><strong>Layout</strong><p>15 components · 43 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--4" href="/marketing"><span class="home-category-index">08</span><div><strong>Marketing</strong><p>10 components · 30 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--1" href="/navigation"><span class="home-category-index">09</span><div><strong>Navigation</strong><p>10 components · 30 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--2" href="/overlays"><span class="home-category-index">010</span><div><strong>Overlays</strong><p>6 components · 18 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a><a class="home-category home-category--3" href="/tables"><span class="home-category-index">011</span><div><strong>Tables</strong><p>1 components · 9 live demos</p></div><span class="icon-[lucide--arrow-up-right] size-5" aria-hidden="true"></span></a></div></section>
</div> </div>
} }
} }
@@ -10,7 +10,7 @@ page LayoutShowcase {
<span class="showcase-eyebrow">Component category</span> <span class="showcase-eyebrow">Component category</span>
<h1 class="showcase-title">Layout</h1> <h1 class="showcase-title">Layout</h1>
<p class="showcase-description">15 unique, responsive, theme-aware components. Open a component to inspect multiple live configurations and its complete props API.</p> <p class="showcase-description">15 unique, responsive, theme-aware components. Open a component to inspect multiple live configurations and its complete props API.</p>
<div class="category-meta"><span>15 components</span><span>Multiple use cases</span><span>45 live configurations</span></div> <div class="category-meta"><span>15 components</span><span>Multiple use cases</span><span>43 live configurations</span></div>
</header> </header>
<div class="catalog-grid"> <div class="catalog-grid">
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
@@ -36,11 +36,11 @@ page LayoutShowcase {
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview"> <div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div> <div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><CustomScrollbar size="default" columns="2" class="showcase-instance showcase-instance--1" /></div> <div class="catalog-card-stage"><CustomScrollbar size="default" axis="vertical" thickness="8" maxHeight="14rem" radius="999px" class="showcase-instance showcase-instance--1"><div class="showcase-scroll-demo"><p>Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p></div></CustomScrollbar></div>
</div> </div>
<div class="catalog-card-body"> <div class="catalog-card-body">
<div><span class="catalog-card-category">Layout</span><h2>Custom Scrollbar</h2><p>Theme-aware, responsive custom scrollbar component.</p></div> <div><span class="catalog-card-category">Layout</span><h2>Custom Scrollbar</h2><p>Theme-aware, responsive custom scrollbar component.</p></div>
<div class="catalog-card-footer"><span>6 props · 1 slots</span><a href="/components/custom-scrollbar">Explore component <span aria-hidden="true">→</span></a></div> <div class="catalog-card-footer"><span>7 props · 1 slots</span><a href="/components/custom-scrollbar">Explore component <span aria-hidden="true">→</span></a></div>
</div> </div>
</article> </article>
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
@@ -106,11 +106,11 @@ page LayoutShowcase {
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview"> <div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div> <div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><LayoutSplitter size="default" columns="2" class="showcase-instance showcase-instance--1" /></div> <div class="catalog-card-stage"><LayoutSplitter size="40" orientation="horizontal" minSize="20" step="5" label="Resize panels" class="showcase-instance showcase-instance--1" /></div>
</div> </div>
<div class="catalog-card-body"> <div class="catalog-card-body">
<div><span class="catalog-card-category">Layout</span><h2>Layout Splitter</h2><p>Theme-aware, responsive layout splitter component.</p></div> <div><span class="catalog-card-category">Layout</span><h2>Layout Splitter</h2><p>Theme-aware, responsive layout splitter component.</p></div>
<div class="catalog-card-footer"><span>6 props · 1 slots</span><a href="/components/layout-splitter">Explore component <span aria-hidden="true">→</span></a></div> <div class="catalog-card-footer"><span>7 props · 3 slots</span><a href="/components/layout-splitter">Explore component <span aria-hidden="true">→</span></a></div>
</div> </div>
</article> </article>
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
@@ -10,7 +10,7 @@ page NavigationShowcase {
<span class="showcase-eyebrow">Component category</span> <span class="showcase-eyebrow">Component category</span>
<h1 class="showcase-title">Navigation</h1> <h1 class="showcase-title">Navigation</h1>
<p class="showcase-description">10 unique, responsive, theme-aware components. Open a component to inspect multiple live configurations and its complete props API.</p> <p class="showcase-description">10 unique, responsive, theme-aware components. Open a component to inspect multiple live configurations and its complete props API.</p>
<div class="category-meta"><span>10 components</span><span>Multiple use cases</span><span>28 live configurations</span></div> <div class="category-meta"><span>10 components</span><span>Multiple use cases</span><span>30 live configurations</span></div>
</header> </header>
<div class="catalog-grid"> <div class="catalog-grid">
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
@@ -56,7 +56,7 @@ page NavigationShowcase {
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview"> <div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div> <div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><Navbar size="default" label="Navbar" topbarLabel="Navbar" brand='({"id":"navbar-0-1","key":"navbar-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]})' items='[{"id":"navbar-0-1","key":"navbar-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"navbar-0-2","key":"navbar-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' actions='[{"id":"navbar-0-1","key":"navbar-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"navbar-0-2","key":"navbar-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#navbar-demo","actionHref":"#navbar-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' sticky="false" openOnHover="false" mobileLabel="Navbar" class="showcase-instance showcase-instance--1" /></div> <div class="catalog-card-stage"><Navbar size="default" label="Primary navigation" topbarLabel="Navbar" brand='{demo_obj_0}' items='[{"label":"Overview","href":"/","value":"overview"},{"label":"Projects","href":"/projects","value":"projects"},{"label":"Reports","href":"/reports","value":"reports"}]' actions='[{"label":"Sign in","href":"/login","variant":"link"},{"label":"Start free","href":"/signup","variant":"primary"}]' active="projects" sticky="false" openOnHover="false" mobileLabel="Navbar" class="showcase-instance showcase-instance--1" /></div>
</div> </div>
<div class="catalog-card-body"> <div class="catalog-card-body">
<div><span class="catalog-card-category">Navigation</span><h2>Navbar</h2><p>Theme-aware, responsive navbar component.</p></div> <div><span class="catalog-card-category">Navigation</span><h2>Navbar</h2><p>Theme-aware, responsive navbar component.</p></div>
@@ -96,11 +96,11 @@ page NavigationShowcase {
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview"> <div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div> <div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><Stepper size="default" steps='[{"label":"Account","description":"Your details"},{"label":"Billing","description":"Payment method"},{"label":"Confirm","description":"Review and submit"}]' active="1" clickable="false" label="Stepper" class="showcase-instance showcase-instance--1" /></div> <div class="catalog-card-stage"><Stepper size="default" steps='[{"label":"Account","description":"Your details"},{"label":"Billing","description":"Payment method"},{"label":"Confirm","description":"Review and submit"}]' active="1" clickable="false" label="Stepper" showPanel="false" controls="false" allowSkip="false" nextDisabled="false" backLabel="Back" nextLabel="Next" skipLabel="Skip" finishLabel="Finish" class="showcase-instance showcase-instance--1" /></div>
</div> </div>
<div class="catalog-card-body"> <div class="catalog-card-body">
<div><span class="catalog-card-category">Navigation</span><h2>Stepper</h2><p>Theme-aware, responsive stepper component.</p></div> <div><span class="catalog-card-category">Navigation</span><h2>Stepper</h2><p>Theme-aware, responsive stepper component.</p></div>
<div class="catalog-card-footer"><span>8 props · 2 slots</span><a href="/components/stepper">Explore component <span aria-hidden="true">→</span></a></div> <div class="catalog-card-footer"><span>16 props · 3 slots</span><a href="/components/stepper">Explore component <span aria-hidden="true">→</span></a></div>
</div> </div>
</article> </article>
<article class="catalog-card" data-interactive-preview="false"> <article class="catalog-card" data-interactive-preview="false">
@@ -2925,3 +2925,66 @@ body {
.demo-canvas:has([data-ui-component="Tooltip"], [data-ui-component="Popover"]) .demo-render { .demo-canvas:has([data-ui-component="Tooltip"], [data-ui-component="Popover"]) .demo-render {
padding-block: 5rem; padding-block: 5rem;
} }
/*
* Scrollspy demo sections.
*
* Real page-flow content on purpose: the runtime observes sections against the
* viewport, so a table of contents inside a small scrolling box would have
* nothing to react to and the demo could not be tested at all.
*/
.showcase-spy-doc {
display: grid;
gap: 0.75rem;
margin-bottom: 1rem;
}
.showcase-spy-section {
min-height: 42vh;
padding: 1rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface-soft);
scroll-margin-top: 5rem;
}
.showcase-spy-section h3 {
margin: 0 0 0.4rem;
font-size: 0.95rem;
font-weight: 700;
}
.showcase-spy-section p {
margin: 0;
color: var(--wire-color-text-muted);
font-size: 0.86rem;
line-height: 1.6;
}
/*
* CustomScrollbar demo content. A scrolling region shows nothing at all unless
* its content overflows, so the demo has to supply enough of it.
*/
.showcase-scroll-demo {
display: grid;
gap: 0.5rem;
padding: 0.25rem;
}
.showcase-scroll-demo p {
margin: 0;
color: var(--wire-color-text-muted);
font-size: 0.85rem;
}
.showcase-scroll-demo--wide {
grid-auto-flow: column;
grid-auto-columns: 9rem;
}
.showcase-scroll-demo--wide span {
padding: 0.75rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
font-size: 0.85rem;
}
@@ -424,16 +424,42 @@ function sampleValue(prop, component, variation) {
return undefined; return undefined;
} }
/*
* Object props are hoisted into page state rather than written inline.
*
* A bare {...} attribute is read by the compiler as an interpolation, and
* escaping the braces does not help either: prop coercion runs JSON.parse on
* the raw attribute, which never sees the entities decoded, so the mount threw
* and the demo silently rendered nothing. Binding to state is what the
* playground already does, and it is the only form that survives both.
*
* Arrays start with [ and are unambiguous, so they stay inline.
*/
const hoistedObjects = [];
function resetHoistedObjects() {
hoistedObjects.length = 0;
}
function hoistObjectProp(value) {
const name = `demo_obj_${hoistedObjects.length}`;
// JSON.parse, not a bare object literal: the parser reads a leading brace
// as the start of a block and rejects the rest of the page.
const json = JSON.stringify(JSON.stringify(value));
hoistedObjects.push(` state ${name} = JSON.parse(${json})`);
return name;
}
function hoistedObjectStates() {
if (!hoistedObjects.length) return "";
// playgroundStates has no trailing newline, so lead with one.
return "\n" + hoistedObjects.join("\n");
}
function wrnAttribute(name, value) { function wrnAttribute(name, value) {
if (typeof value === "object" && value !== null) { if (typeof value === "object" && value !== null) {
const expression = JSON.stringify(value).replaceAll("'", "\\u0027"); if (!Array.isArray(value)) return `${name}='{${hoistObjectProp(value)}}'`;
/* return `${name}='${JSON.stringify(value).replaceAll("'", "\\u0027")}'`;
* An object literal has to be parenthesised. A bare {...} at the start of
* an attribute is read as an interpolation, so the compiler tried to parse
* the JSON as JavaScript and stopped at the first colon. Arrays start with
* [ and were never affected, which is why only object props broke.
*/
return `${name}='${Array.isArray(value) ? expression : `(${expression})`}'`;
} }
return `${name}="${escapeAttribute(String(value))}"`; return `${name}="${escapeAttribute(String(value))}"`;
} }
@@ -1475,6 +1501,8 @@ function eventDocumentation(component) {
} }
function detailPage(component, categoryComponents) { function detailPage(component, categoryComponents) {
// Names are per page; without this they accumulate across the whole run.
resetHoistedObjects();
const index = categoryComponents.findIndex((item) => item.name === component.name); const index = categoryComponents.findIndex((item) => item.name === component.name);
const previous = categoryComponents[index - 1]; const previous = categoryComponents[index - 1];
const next = categoryComponents[index + 1]; const next = categoryComponents[index + 1];
@@ -1552,7 +1580,7 @@ function detailPage(component, categoryComponents) {
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly. return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page ${component.name.replace(/[^A-Za-z0-9_]/g, "")}Detail { page ${component.name.replace(/[^A-Za-z0-9_]/g, "")}Detail {
layout = "showcase" layout = "showcase"
${playgroundStates(component)} ${playgroundStates(component)}${hoistedObjectStates()}
seo { seo {
title = "${escapeAttribute(displayName(component.name))}" title = "${escapeAttribute(displayName(component.name))}"
description = "${escapeAttribute(component.purpose)}" description = "${escapeAttribute(component.purpose)}"
@@ -253,12 +253,133 @@ const DATATABLE_ROWS =
'[{"id": 1, "name": "Northwind", "plan": "Scale", "owner": "A. Okafor", "seats": 6, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 2, "name": "Acme Industrial", "plan": "Team", "owner": "R. Silva", "seats": 13, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 3, "name": "Globex", "plan": "Enterprise", "owner": "M. Chen", "seats": 20, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 4, "name": "Initech", "plan": "Starter", "owner": "J. Dubois", "seats": 27, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 5, "name": "Umbrella", "plan": "Scale", "owner": "P. Novak", "seats": 34, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 6, "name": "Stark Labs", "plan": "Team", "owner": "A. Okafor", "seats": 41, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 7, "name": "Wayne Foods", "plan": "Enterprise", "owner": "R. Silva", "seats": 48, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 8, "name": "Soylent", "plan": "Starter", "owner": "M. Chen", "seats": 55, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 9, "name": "Hooli", "plan": "Scale", "owner": "J. Dubois", "seats": 62, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 10, "name": "Vehement", "plan": "Team", "owner": "P. Novak", "seats": 69, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 11, "name": "Massive Dynamic", "plan": "Enterprise", "owner": "A. Okafor", "seats": 76, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 12, "name": "Cyberdyne", "plan": "Starter", "owner": "R. Silva", "seats": 83, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 13, "name": "Tyrell", "plan": "Scale", "owner": "M. Chen", "seats": 90, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 14, "name": "Aperture", "plan": "Team", "owner": "J. Dubois", "seats": 97, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 15, "name": "Black Mesa", "plan": "Enterprise", "owner": "P. Novak", "seats": 104, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}]'; '[{"id": 1, "name": "Northwind", "plan": "Scale", "owner": "A. Okafor", "seats": 6, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 2, "name": "Acme Industrial", "plan": "Team", "owner": "R. Silva", "seats": 13, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 3, "name": "Globex", "plan": "Enterprise", "owner": "M. Chen", "seats": 20, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 4, "name": "Initech", "plan": "Starter", "owner": "J. Dubois", "seats": 27, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 5, "name": "Umbrella", "plan": "Scale", "owner": "P. Novak", "seats": 34, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 6, "name": "Stark Labs", "plan": "Team", "owner": "A. Okafor", "seats": 41, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 7, "name": "Wayne Foods", "plan": "Enterprise", "owner": "R. Silva", "seats": 48, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 8, "name": "Soylent", "plan": "Starter", "owner": "M. Chen", "seats": 55, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 9, "name": "Hooli", "plan": "Scale", "owner": "J. Dubois", "seats": 62, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 10, "name": "Vehement", "plan": "Team", "owner": "P. Novak", "seats": 69, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 11, "name": "Massive Dynamic", "plan": "Enterprise", "owner": "A. Okafor", "seats": 76, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 12, "name": "Cyberdyne", "plan": "Starter", "owner": "R. Silva", "seats": 83, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 13, "name": "Tyrell", "plan": "Scale", "owner": "M. Chen", "seats": 90, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 14, "name": "Aperture", "plan": "Team", "owner": "J. Dubois", "seats": 97, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 15, "name": "Black Mesa", "plan": "Enterprise", "owner": "P. Novak", "seats": 104, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}]';
export const componentProfiles = { export const componentProfiles = {
Scrollspy: { Navbar: {
demos: [ demos: [
standard( standard(
"Table of contents", "Brand, links and actions",
"Each href points at an element on the page. The runtime observes those elements and moves aria-current to the link for whichever one is in view, so the marker follows the reader without any component state.", "The everyday shape: a brand, a row of links with the current page marked, and calls to action on the right. Arrow keys move along the menu bar.",
{ {
brand: { label: "Northwind", description: "Console", href: "/" },
items: [
{ label: "Overview", href: "/", value: "overview" },
{ label: "Projects", href: "/projects", value: "projects" },
{ label: "Reports", href: "/reports", value: "reports" },
],
actions: [
{ label: "Sign in", href: "/login", variant: "link" },
{ label: "Start free", href: "/signup", variant: "primary" },
],
active: "projects",
label: "Primary navigation",
},
),
advanced(
"Dropdown and mega panel",
"An item carrying children opens a panel built from native details and summary, so it is keyboard operable without any extra script. Give a child its own children to get a titled column inside the panel.",
{
brand: { label: "Northwind", icon: "icon-[lucide--box]", href: "/" },
items: [
{ label: "Overview", href: "/", value: "overview" },
{
label: "Products",
value: "products",
columns: 2,
description: "Everything in the platform.",
children: [
{
label: "Platform",
children: [
{
label: "Runtime",
href: "/runtime",
description: "The browser half.",
icon: "icon-[lucide--cpu]",
},
{
label: "Compiler",
href: "/compiler",
description: "WRN to JavaScript.",
icon: "icon-[lucide--hammer]",
},
],
},
{
label: "Tooling",
children: [
{ label: "CLI", href: "/cli", description: "Scaffold and deploy." },
{ label: "Editor", href: "/editor", description: "Language server." },
],
},
],
},
{ label: "Pricing", href: "/pricing", value: "pricing" },
],
actions: [{ label: "Book a demo", href: "/demo", variant: "primary" }],
active: "overview",
},
),
compact(
"Sticky and compact",
"Sticky keeps the bar pinned while the page scrolls. The toggle appears below the tablet breakpoint and the links collapse behind it.",
{
brand: { label: "Acme", href: "/" },
items: [
{ label: "Docs", href: "/docs", value: "docs" },
{ label: "Support", href: "/support", value: "support" },
],
actions: [{ label: "Dashboard", href: "/app", variant: "primary" }],
active: "docs",
sticky: true,
size: "sm",
mobileLabel: "Toggle navigation",
},
),
],
},
LayoutSplitter: {
demos: [
standard(
"Drag or arrow the divider",
"Two panes with a movable divider. Drag it, or focus it and use the arrow keys; Home and End go to the bounds. The size is resolved in the runtime and written onto the container as a custom property, because a pointermove fires far too often to route through a client function.",
{ size: 40, minSize: 20, step: 5, orientation: "horizontal", label: "Resize panels" },
),
advanced(
"Stacked panes",
"Vertical orientation splits top from bottom and switches the arrow keys to up and down. Below the small breakpoint both orientations stack, because two panes side by side stop making sense on a phone.",
{ size: 60, minSize: 25, step: 10, orientation: "vertical", label: "Resize rows" },
),
],
},
CustomScrollbar: {
demos: [
standard(
"Themed scrolling region",
"Scrollbar appearance is CSS rather than script: scrollbar-width and scrollbar-color are the standard properties, with webkit rules for the engines that still need them. The thumb follows the component colour.",
{ axis: "vertical", thickness: 8, maxHeight: "14rem", radius: "999px" },
{
default: `<div class="showcase-scroll-demo"><p>Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p></div>`,
},
),
advanced(
"Horizontal, thicker track",
"A horizontal region with a heavier track. thickness is clamped, since it arrives as an attribute and a scrollbar wider than its content helps nobody.",
{ axis: "horizontal", thickness: 14, maxHeight: "8rem", radius: "4px", color: "info" },
{
default: `<div class="showcase-scroll-demo showcase-scroll-demo--wide"><span>Column 1</span><span>Column 2</span><span>Column 3</span><span>Column 4</span><span>Column 5</span><span>Column 6</span><span>Column 7</span><span>Column 8</span><span>Column 9</span><span>Column 10</span><span>Column 11</span><span>Column 12</span><span>Column 13</span><span>Column 14</span></div>`,
},
),
],
},
Scrollspy: {
demos: [
{
eyebrow: "Recommended",
title: "Follows the reader",
description:
"Scroll the page and the marker moves to whichever section is in view. The sections below are real page content, because the runtime observes against the viewport -- a table of contents inside a small scrolling box would have nothing to react to.",
before:
'<div class="showcase-spy-doc"><section id="overview" class="showcase-spy-section"><h3>Overview</h3><p>Scroll this page and watch the marker in the list move. The runtime observes these sections against the viewport, so the current link follows whatever you are reading.</p></section><section id="installation" class="showcase-spy-section"><h3>Installation</h3><p>Each link points at one of these ids. Nothing is wired up by hand -- the component only renders the links, and the runtime moves aria-current.</p></section><section id="usage" class="showcase-spy-section"><h3>Usage</h3><p>The active link is marked with aria-current="location", which is what a screen reader announces, and styled through data-active.</p></section><section id="api" class="showcase-spy-section"><h3>API</h3><p>Clicking a link jumps to its section and marks it immediately, so the control responds even before the scroll settles.</p></section></div>',
props: {
heading: "On this page", heading: "On this page",
items: [ items: [
{ label: "Overview", href: "#overview" }, { label: "Overview", href: "#overview" },
@@ -268,7 +389,8 @@ export const componentProfiles = {
], ],
active: "#overview", active: "#overview",
}, },
), slots: {},
},
advanced( advanced(
"Compact rail", "Compact rail",
"On a phone the rail becomes a horizontal strip that scrolls, so a long contents list costs one line rather than a screenful.", "On a phone the rail becomes a horizontal strip that scrolls, so a long contents list costs one line rather than a screenful.",
@@ -276,11 +398,11 @@ export const componentProfiles = {
heading: "Sections", heading: "Sections",
size: "sm", size: "sm",
items: [ items: [
{ label: "Getting started", href: "#getting-started" }, { label: "Overview", href: "#overview" },
{ label: "Configuration", href: "#configuration" }, { label: "Installation", href: "#installation" },
{ label: "Troubleshooting", href: "#troubleshooting" }, { label: "Usage", href: "#usage" },
], ],
active: "#configuration", active: "#installation",
}, },
), ),
], ],
@@ -470,6 +592,15 @@ export const componentProfiles = {
{ label: "Confirm", description: "Review and submit" }, { label: "Confirm", description: "Review and submit" },
], ],
active: 1, active: 1,
clickable: false,
showPanel: false,
controls: false,
allowSkip: false,
nextDisabled: false,
backLabel: "Back",
nextLabel: "Next",
skipLabel: "Skip",
finishLabel: "Finish",
}, },
), ),
standard( standard(
@@ -483,6 +614,56 @@ export const componentProfiles = {
], ],
active: 2, active: 2,
orientation: "vertical", orientation: "vertical",
clickable: false,
showPanel: false,
controls: false,
allowSkip: false,
nextDisabled: false,
backLabel: "Back",
nextLabel: "Next",
skipLabel: "Skip",
finishLabel: "Finish",
},
),
advanced(
"Wizard with content and controls",
"showPanel renders each step body and shows only the active one, the same contract Tabs uses. controls adds Back, Skip and Next, which becomes Finish on the last step.",
{
steps: [
{ label: "Account", title: "Your details", content: "Name, email and a password." },
{ label: "Billing", title: "How you pay", content: "Card or invoice." },
{ label: "Confirm", title: "Review", content: "Check everything before submitting." },
],
active: 1,
clickable: false,
showPanel: true,
controls: true,
allowSkip: true,
nextDisabled: false,
backLabel: "Back",
nextLabel: "Next",
skipLabel: "Skip",
finishLabel: "Finish",
},
),
advanced(
"Held until the step is valid",
"The same wizard with nextDisabled set. The component never validates anything itself: the page owns the form, sets this while the step is incomplete, and clears it once the step passes.",
{
steps: [
{ label: "Account", title: "Your details", content: "This step is not complete yet." },
{ label: "Billing", title: "How you pay", content: "Card or invoice." },
],
active: 0,
clickable: false,
showPanel: true,
controls: true,
allowSkip: false,
nextDisabled: true,
backLabel: "Back",
nextLabel: "Next",
skipLabel: "Skip",
finishLabel: "Finish",
}, },
), ),
advanced( advanced(
@@ -492,6 +673,14 @@ export const componentProfiles = {
steps: [{ label: "One" }, { label: "Two" }, { label: "Three" }], steps: [{ label: "One" }, { label: "Two" }, { label: "Three" }],
active: 0, active: 0,
clickable: true, clickable: true,
showPanel: false,
controls: false,
allowSkip: false,
nextDisabled: false,
backLabel: "Back",
nextLabel: "Next",
skipLabel: "Skip",
finishLabel: "Finish",
}, },
), ),
], ],
@@ -393,11 +393,11 @@
"slug": "custom-scrollbar", "slug": "custom-scrollbar",
"category": "layout", "category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component.", "purpose": "Theme-aware, responsive custom scrollbar component.",
"demoCount": 3, "demoCount": 2,
"propCount": 6, "propCount": 7,
"slots": ["default"], "slots": ["default"],
"events": ["scroll"], "events": [],
"profiled": false "profiled": true
}, },
{ {
"name": "DataMap", "name": "DataMap",
@@ -690,11 +690,11 @@
"slug": "layout-splitter", "slug": "layout-splitter",
"category": "layout", "category": "layout",
"purpose": "Theme-aware, responsive layout splitter component.", "purpose": "Theme-aware, responsive layout splitter component.",
"demoCount": 3, "demoCount": 2,
"propCount": 6, "propCount": 7,
"slots": ["default"], "slots": ["start", "end", "default"],
"events": ["resizeStart", "resize", "resizeEnd"], "events": ["resize"],
"profiled": false "profiled": true
}, },
{ {
"name": "LegendIndicator", "name": "LegendIndicator",
@@ -850,7 +850,7 @@
"propCount": 13, "propCount": 13,
"slots": ["topbar", "actions"], "slots": ["topbar", "actions"],
"events": ["toggle", "open", "close", "select", "action"], "events": ["toggle", "open", "close", "select", "action"],
"profiled": false "profiled": true
}, },
{ {
"name": "PageHeader", "name": "PageHeader",
@@ -1110,10 +1110,10 @@
"slug": "stepper", "slug": "stepper",
"category": "navigation", "category": "navigation",
"purpose": "Theme-aware, responsive stepper component.", "purpose": "Theme-aware, responsive stepper component.",
"demoCount": 3, "demoCount": 5,
"propCount": 8, "propCount": 16,
"slots": ["step-{index}", "default"], "slots": ["step-{index}", "panel-{index}", "default"],
"events": ["change"], "events": ["change", "back", "next", "skip", "finish"],
"profiled": true "profiled": true
}, },
{ {
+119 -81
View File
@@ -2953,6 +2953,15 @@ export const REACTIVE_RUNTIME = String.raw`
if (!dialog || !dialog.isConnected) return false; if (!dialog || !dialog.isConnected) return false;
if (dialog.hasAttribute("hidden")) return false; if (dialog.hasAttribute("hidden")) return false;
if (dialog.closest('[data-show="false"]')) return false; if (dialog.closest('[data-show="false"]')) return false;
/*
* data-open is the signal Modal and Drawer actually publish, and it is the
* only one Drawer can publish: its panel animates open, so it cannot be
* toggled with data-show, which sets display:none. Treating every dialog
* without a data-show="false" ancestor as open meant a closed Drawer held
* the body scroll lock forever and the page could not be scrolled.
*/
var owner = dialog.closest("[data-open]");
if (owner) return owner.getAttribute("data-open") === "true";
return true; return true;
} }
@@ -3254,6 +3263,115 @@ export const REACTIVE_RUNTIME = String.raw`
}); });
} }
/*
* Resizable split panes.
*
* The resolved size is written onto the container as a --wrn-split custom
* property and the component styles from it. Pointer moves fire far too
* often to route through a client function, and a state write made inside a
* pointermove callback is dropped, so the DOM holds the answer.
*/
var SPLITTER_SELECTOR = "[data-wrn-splitter]";
function splitterNumber(element, name, fallback) {
var raw = Number(element.getAttribute(name));
return isFinite(raw) && raw !== 0 ? raw : fallback;
}
function splitterBounds(root) {
var min = splitterNumber(root, "data-wrn-splitter-min", 10);
return { min: min, max: 100 - min };
}
function applySplit(root, handle, size) {
var bounds = splitterBounds(root);
var next = Math.min(bounds.max, Math.max(bounds.min, size));
next = Math.round(next * 100) / 100;
root.style.setProperty("--wrn-split", next + "%");
if (handle) {
handle.setAttribute("aria-valuenow", String(next));
handle.setAttribute("aria-valuemin", String(bounds.min));
handle.setAttribute("aria-valuemax", String(bounds.max));
}
// Named for the component output so a parent @resize binding receives it.
root.dispatchEvent(new CustomEvent("resize", { detail: { size: next } }));
return next;
}
function currentSplit(root, handle) {
var fromHandle = Number(handle && handle.getAttribute("aria-valuenow"));
if (isFinite(fromHandle) && fromHandle) return fromHandle;
var raw = String(root.style.getPropertyValue("--wrn-split") || "").replace("%", "");
var parsed = Number(raw);
return isFinite(parsed) && parsed ? parsed : 50;
}
function handleSplitterKeydown(event) {
var target = event.target;
if (!target || !target.closest) return;
var handle = target.closest("[data-wrn-splitter-handle]");
if (!handle) return;
var root = handle.closest(SPLITTER_SELECTOR);
if (!root) return;
var vertical = root.getAttribute("data-wrn-splitter") === "vertical";
var step = splitterNumber(root, "data-wrn-splitter-step", 5);
var bounds = splitterBounds(root);
var size = currentSplit(root, handle);
var key = event.key;
var next = size;
if (key === (vertical ? "ArrowDown" : "ArrowRight")) next = size + step;
else if (key === (vertical ? "ArrowUp" : "ArrowLeft")) next = size - step;
else if (key === "Home") next = bounds.min;
else if (key === "End") next = bounds.max;
else return;
event.preventDefault();
applySplit(root, handle, next);
}
function setupSplitters() {
if (window.__wrnexusSplitterBound) return;
window.__wrnexusSplitterBound = true;
document.addEventListener("keydown", handleSplitterKeydown, true);
var dragging = null;
document.addEventListener("pointerdown", function (event) {
var target = event.target;
var handle = target && target.closest ? target.closest("[data-wrn-splitter-handle]") : null;
if (!handle) return;
var root = handle.closest(SPLITTER_SELECTOR);
if (!root) return;
dragging = { root: root, handle: handle };
root.setAttribute("data-wrn-splitter-dragging", "true");
if (handle.setPointerCapture && event.pointerId !== undefined) {
handle.setPointerCapture(event.pointerId);
}
event.preventDefault();
});
document.addEventListener("pointermove", function (event) {
if (!dragging) return;
var rect = dragging.root.getBoundingClientRect();
var vertical = dragging.root.getAttribute("data-wrn-splitter") === "vertical";
var span = vertical ? rect.height : rect.width;
if (!span) return;
var offset = vertical ? event.clientY - rect.top : event.clientX - rect.left;
applySplit(dragging.root, dragging.handle, (offset / span) * 100);
});
var endDrag = function () {
if (!dragging) return;
dragging.root.removeAttribute("data-wrn-splitter-dragging");
dragging = null;
};
document.addEventListener("pointerup", endDrag);
document.addEventListener("pointercancel", endDrag);
}
function hydrateScopes(root) { function hydrateScopes(root) {
var host = root || document; var host = root || document;
@@ -3277,93 +3395,12 @@ export const REACTIVE_RUNTIME = String.raw`
ensureBehaviorObserver(); ensureBehaviorObserver();
hydrateNavbarControllers(host); hydrateNavbarControllers(host);
hydratePreferenceControllers(host); hydratePreferenceControllers(host);
hydrateSidebarControllers(host);
hydrateDropdownControllers(host);
hydrateSelectControllers(host); hydrateSelectControllers(host);
hydratePinInputControllers(host); hydratePinInputControllers(host);
} }
var navbarOutsideClickBound = false; var navbarOutsideClickBound = false;
var preferenceOutsideClickBound = false; var preferenceOutsideClickBound = false;
var dropdownOutsideClickBound = false;
function hydrateSidebarControllers(root) {
var host = root || document;
var sidebars = [];
if (host.nodeType === 1 && host.matches && host.matches(".wire-sidebar-shell")) {
sidebars.push(host);
}
if (host.querySelectorAll) {
Array.prototype.push.apply(sidebars, host.querySelectorAll(".wire-sidebar-shell"));
}
sidebars.forEach(function (sidebar) {
var current = window.location.pathname.replace(/\/+$/, "") || "/";
var best = null;
var bestLength = -1;
sidebar.querySelectorAll(".wire-sidebar-items a[href]").forEach(function (link) {
var url;
try { url = new URL(link.getAttribute("href"), window.location.origin); } catch (_) { return; }
var path = url.pathname.replace(/\/+$/, "") || "/";
var matches = path === current || (path !== "/" && current.indexOf(path + "/") === 0);
link.classList.remove("is-active");
if (!matches || path.length <= bestLength) return;
best = link;
bestLength = path.length;
});
sidebar.querySelectorAll(".wire-sidebar-items a[aria-current='page']").forEach(function (link) {
if (link !== best) link.removeAttribute("aria-current");
});
sidebar.querySelectorAll(".wire-sidebar-group.has-active").forEach(function (group) {
group.classList.remove("has-active");
});
if (best) {
best.classList.add("is-active");
best.setAttribute("aria-current", "page");
var group = best.closest(".wire-sidebar-group");
if (group) {
group.setAttribute("open", "");
group.classList.add("has-active");
}
}
});
}
function hydrateDropdownControllers(root) {
var host = root || document;
var dropdowns = [];
if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-dropdown]")) {
dropdowns.push(host);
}
if (host.querySelectorAll) {
Array.prototype.push.apply(dropdowns, host.querySelectorAll("[data-wrn-dropdown]"));
}
dropdowns.forEach(function (dropdown) {
if (dropdown.dataset.wrnDropdownBound === "true") return;
dropdown.dataset.wrnDropdownBound = "true";
dropdown.addEventListener("toggle", function () {
if (!dropdown.open) return;
document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (candidate) {
if (candidate !== dropdown) candidate.removeAttribute("open");
});
});
});
if (!dropdownOutsideClickBound) {
dropdownOutsideClickBound = true;
document.addEventListener("pointerdown", function (event) {
document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (dropdown) {
if (!dropdown.contains(event.target)) dropdown.removeAttribute("open");
});
});
document.addEventListener("keydown", function (event) {
if (event.key !== "Escape") return;
document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (dropdown) {
dropdown.removeAttribute("open");
var summary = dropdown.querySelector(":scope > summary");
if (summary) summary.focus();
});
});
}
}
function hydratePreferenceControllers(root) { function hydratePreferenceControllers(root) {
var host = root || document; var host = root || document;
@@ -5473,6 +5510,7 @@ export const REACTIVE_RUNTIME = String.raw`
setupModalDialogs(); setupModalDialogs();
setupRovingFocus(); setupRovingFocus();
setupScrollspy(); setupScrollspy();
setupSplitters();
startDocumentWatch(); startDocumentWatch();
window.__wrnexusRepositionAnchored = repositionAnchored; window.__wrnexusRepositionAnchored = repositionAnchored;
window.__wrnexusHydrateScopes = hydrateScopes; window.__wrnexusHydrateScopes = hydrateScopes;
+127
View File
@@ -16,6 +16,14 @@ function mount(html: string): Window {
(globalThis as Record<string, unknown>).MutationObserver = ( (globalThis as Record<string, unknown>).MutationObserver = (
win as unknown as { MutationObserver: unknown } win as unknown as { MutationObserver: unknown }
).MutationObserver; ).MutationObserver;
/*
* Bind the window CustomEvent too. Without it the runtime constructs events
* from the host global, and a listener registered through happy-dom never
* matches them, so anything dispatched looks silently lost.
*/
(globalThis as Record<string, unknown>).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
(0, eval)(REACTIVE_RUNTIME); (0, eval)(REACTIVE_RUNTIME);
// Hydrate deterministically (auto-init waits on DOMContentLoaded, which the // Hydrate deterministically (auto-init waits on DOMContentLoaded, which the
// test window may not fire). setupScope is idempotent, so this is safe. // test window may not fire). setupScope is idempotent, so this is safe.
@@ -1067,3 +1075,122 @@ test("json inside a textarea is left alone, not read as mustaches", () => {
// evaluated {"id":"a-1","count":3} away and left it empty. // evaluated {"id":"a-1","count":3} away and left it empty.
expect(doc.querySelector("#editor")!.textContent).toBe('{"id":"a-1","count":3}'); expect(doc.querySelector("#editor")!.textContent).toBe('{"id":"a-1","count":3}');
}); });
test("a closed drawer does not hold the body scroll lock", () => {
const win = mount(
`<div data-ui-component="Drawer" data-open="false">
<div class="layer">
<section role="dialog" aria-modal="true" tabindex="-1">
<button id="inside">Inside</button>
</section>
</div>
</div>`,
);
const doc = win.document;
/*
* A drawer animates open, so its panel cannot be hidden with data-show --
* display:none is not transitionable. It publishes data-open instead. Reading
* only data-show made every closed drawer look open, which locked the body
* and left the page unscrollable.
*/
expect(doc.body.style.overflow).not.toBe("hidden");
const outside = doc.querySelector("#inside") as unknown as HTMLElement;
outside.focus();
outside.dispatchEvent(keydown(win, "Tab"));
// No trap either: focus is free to leave a closed dialog.
expect(doc.activeElement!.id).toBe("inside");
});
test("an open drawer locks the body scroll and traps Tab", () => {
const win = mount(
`<div data-ui-component="Drawer" data-open="true">
<section role="dialog" aria-modal="true" tabindex="-1">
<button id="first">First</button>
<button id="last">Last</button>
</section>
</div>`,
);
const doc = win.document;
expect(doc.body.style.overflow).toBe("hidden");
const last = doc.querySelector("#last") as unknown as HTMLElement;
last.focus();
last.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("first");
});
test("splitter keyboard steps move the divider and clamp at the bounds", () => {
const win = mount(
`<div data-wrn-splitter="horizontal" data-wrn-splitter-min="20" data-wrn-splitter-step="10"
style="--wrn-split: 50%">
<div>left</div>
<div data-wrn-splitter-handle role="separator" tabindex="0"
aria-valuenow="50" aria-valuemin="20" aria-valuemax="80"></div>
<div>right</div>
</div>`,
);
const doc = win.document;
const root = doc.querySelector("[data-wrn-splitter]") as unknown as HTMLElement;
const handle = doc.querySelector("[data-wrn-splitter-handle]") as unknown as HTMLElement;
handle.dispatchEvent(keydown(win, "ArrowRight"));
expect(handle.getAttribute("aria-valuenow")).toBe("60");
expect(root.style.getPropertyValue("--wrn-split").trim()).toBe("60%");
handle.dispatchEvent(keydown(win, "ArrowLeft"));
expect(handle.getAttribute("aria-valuenow")).toBe("50");
// Home and End go to the bounds, not to 0 and 100: a pane dragged to
// nothing cannot be recovered with a pointer.
handle.dispatchEvent(keydown(win, "Home"));
expect(handle.getAttribute("aria-valuenow")).toBe("20");
handle.dispatchEvent(keydown(win, "End"));
expect(handle.getAttribute("aria-valuenow")).toBe("80");
// Already at the maximum; another step must not exceed it.
handle.dispatchEvent(keydown(win, "ArrowRight"));
expect(handle.getAttribute("aria-valuenow")).toBe("80");
});
test("a vertical splitter responds to up and down instead", () => {
const win = mount(
`<div data-wrn-splitter="vertical" data-wrn-splitter-min="25" data-wrn-splitter-step="5"
style="--wrn-split: 50%">
<div>top</div>
<div data-wrn-splitter-handle role="separator" tabindex="0"
aria-valuenow="50" aria-valuemin="25" aria-valuemax="75"></div>
<div>bottom</div>
</div>`,
);
const handle = win.document.querySelector("[data-wrn-splitter-handle]") as unknown as HTMLElement;
handle.dispatchEvent(keydown(win, "ArrowDown"));
expect(handle.getAttribute("aria-valuenow")).toBe("55");
// The other axis is left alone so the page can still scroll sideways.
handle.dispatchEvent(keydown(win, "ArrowRight"));
expect(handle.getAttribute("aria-valuenow")).toBe("55");
});
test("splitter emits a resize event carrying the new size", () => {
const win = mount(
`<div data-wrn-splitter="horizontal" data-wrn-splitter-min="10" data-wrn-splitter-step="10"
style="--wrn-split: 50%">
<div>left</div>
<div data-wrn-splitter-handle role="separator" tabindex="0"
aria-valuenow="50" aria-valuemin="10" aria-valuemax="90"></div>
<div>right</div>
</div>`,
);
const doc = win.document;
const root = doc.querySelector("[data-wrn-splitter]") as unknown as HTMLElement;
const seen: number[] = [];
// A resize listener is typed as UIEvent, so the detail needs the wider cast.
root.addEventListener("resize", (event) =>
seen.push((event as unknown as CustomEvent).detail.size),
);
(doc.querySelector("[data-wrn-splitter-handle]") as unknown as HTMLElement).dispatchEvent(
keydown(win, "ArrowRight"),
);
expect(seen).toEqual([60]);
});
+8 -8
View File
@@ -633,9 +633,9 @@ Constrain and align page content with responsive gutters and compact, wide, or f
Theme-aware, responsive custom scrollbar component. Theme-aware, responsive custom scrollbar component.
- Mount: `data-component="CustomScrollbar"` - Mount: `data-component="CustomScrollbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""` - Props: `color: string = "primary"`, `size: string = "default"`, `axis: string = "vertical"`, `thickness: number = 8`, `maxHeight: string = "20rem"`, `radius: string = "999px"`, `class: string = ""`
- Slots: `default` - Slots: `default`
- Outputs: `scroll({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])` - Outputs: None
### Divider ### Divider
@@ -696,9 +696,9 @@ Theme-aware, responsive kbd component.
Theme-aware, responsive layout splitter component. Theme-aware, responsive layout splitter component.
- Mount: `data-component="LayoutSplitter"` - Mount: `data-component="LayoutSplitter"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""` - Props: `color: string = "primary"`, `size: number = 50`, `orientation: string = "horizontal"`, `minSize: number = 15`, `step: number = 5`, `label: string = "Resize panels"`, `class: string = ""`
- Slots: `default` - Slots: `start`, `end`, `default`
- Outputs: `resizeStart({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resize({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resizeEnd({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])` - Outputs: `resize({ size: number })`
### Link ### Link
@@ -916,9 +916,9 @@ Theme-aware, responsive sidebar component.
Theme-aware, responsive stepper component. Theme-aware, responsive stepper component.
- Mount: `data-component="Stepper"` - Mount: `data-component="Stepper"`
- Props: `color: string = "primary"`, `size: string = "default"`, `steps: unknown[] = []`, `active: number = 0`, `orientation: string = "horizontal"`, `clickable: boolean = false`, `label: string = "Progress"`, `class: string = ""` - Props: `color: string = "primary"`, `size: string = "default"`, `steps: unknown[] = []`, `active: number = 0`, `orientation: string = "horizontal"`, `clickable: boolean = false`, `label: string = "Progress"`, `showPanel: boolean = false`, `controls: boolean = false`, `allowSkip: boolean = false`, `nextDisabled: boolean = false`, `backLabel: string = "Back"`, `nextLabel: string = "Next"`, `skipLabel: string = "Skip"`, `finishLabel: string = "Finish"`, `class: string = ""`
- Slots: `step-{index}`, `default` - Slots: `step-{index}`, `panel-{index}`, `default`
- Outputs: `change({ index: number; step: object })` - Outputs: `change({ index: number; step: object })`, `back({ index: number; step: object })`, `next({ index: number; step: object })`, `skip({ index: number; step: object })`, `finish({ index: number; step: object })`
### Tabs ### Tabs
+119 -46
View File
@@ -4443,13 +4443,6 @@
"category": "layout", "category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component.", "purpose": "Theme-aware, responsive custom scrollbar component.",
"props": [ "props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{ {
"name": "color", "name": "color",
"type": "string", "type": "string",
@@ -4458,24 +4451,38 @@
"options": [] "options": []
}, },
{ {
"name": "columns", "name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "axis",
"type": "string",
"required": false,
"default": "\"vertical\"",
"options": []
},
{
"name": "thickness",
"type": "number", "type": "number",
"required": false, "required": false,
"default": "2", "default": "8",
"options": [] "options": []
}, },
{ {
"name": "gap", "name": "maxHeight",
"type": "string", "type": "string",
"required": false, "required": false,
"default": "\"md\"", "default": "\"20rem\"",
"options": [] "options": []
}, },
{ {
"name": "maxWidth", "name": "radius",
"type": "string", "type": "string",
"required": false, "required": false,
"default": "\"xl\"", "default": "\"999px\"",
"options": [] "options": []
}, },
{ {
@@ -4487,13 +4494,8 @@
} }
], ],
"slots": ["default"], "slots": ["default"],
"outputs": [ "outputs": [],
{ "events": [],
"name": "scroll",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
}
],
"events": ["scroll"],
"source": "components/CustomScrollbar.wrn" "source": "components/CustomScrollbar.wrn"
}, },
{ {
@@ -7772,13 +7774,6 @@
"category": "layout", "category": "layout",
"purpose": "Theme-aware, responsive layout splitter component.", "purpose": "Theme-aware, responsive layout splitter component.",
"props": [ "props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{ {
"name": "color", "name": "color",
"type": "string", "type": "string",
@@ -7787,24 +7782,38 @@
"options": [] "options": []
}, },
{ {
"name": "columns", "name": "size",
"type": "number", "type": "number",
"required": false, "required": false,
"default": "2", "default": "50",
"options": [] "options": []
}, },
{ {
"name": "gap", "name": "orientation",
"type": "string", "type": "string",
"required": false, "required": false,
"default": "\"md\"", "default": "\"horizontal\"",
"options": [] "options": []
}, },
{ {
"name": "maxWidth", "name": "minSize",
"type": "number",
"required": false,
"default": "15",
"options": []
},
{
"name": "step",
"type": "number",
"required": false,
"default": "5",
"options": []
},
{
"name": "label",
"type": "string", "type": "string",
"required": false, "required": false,
"default": "\"xl\"", "default": "\"Resize panels\"",
"options": [] "options": []
}, },
{ {
@@ -7815,22 +7824,14 @@
"options": [] "options": []
} }
], ],
"slots": ["default"], "slots": ["start", "end", "default"],
"outputs": [ "outputs": [
{
"name": "resizeStart",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
},
{ {
"name": "resize", "name": "resize",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]" "payloadType": "{ size: number }"
},
{
"name": "resizeEnd",
"payloadType": "{ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]"
} }
], ],
"events": ["resizeStart", "resize", "resizeEnd"], "events": ["resize"],
"source": "components/LayoutSplitter.wrn" "source": "components/LayoutSplitter.wrn"
}, },
{ {
@@ -11783,6 +11784,62 @@
"default": "\"Progress\"", "default": "\"Progress\"",
"options": [] "options": []
}, },
{
"name": "showPanel",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "controls",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "allowSkip",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "nextDisabled",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "backLabel",
"type": "string",
"required": false,
"default": "\"Back\"",
"options": []
},
{
"name": "nextLabel",
"type": "string",
"required": false,
"default": "\"Next\"",
"options": []
},
{
"name": "skipLabel",
"type": "string",
"required": false,
"default": "\"Skip\"",
"options": []
},
{
"name": "finishLabel",
"type": "string",
"required": false,
"default": "\"Finish\"",
"options": []
},
{ {
"name": "class", "name": "class",
"type": "string", "type": "string",
@@ -11791,14 +11848,30 @@
"options": [] "options": []
} }
], ],
"slots": ["step-{index}", "default"], "slots": ["step-{index}", "panel-{index}", "default"],
"outputs": [ "outputs": [
{ {
"name": "change", "name": "change",
"payloadType": "{ index: number; step: object }" "payloadType": "{ index: number; step: object }"
},
{
"name": "back",
"payloadType": "{ index: number; step: object }"
},
{
"name": "next",
"payloadType": "{ index: number; step: object }"
},
{
"name": "skip",
"payloadType": "{ index: number; step: object }"
},
{
"name": "finish",
"payloadType": "{ index: number; step: object }"
} }
], ],
"events": ["change"], "events": ["change", "back", "next", "skip", "finish"],
"source": "components/Stepper.wrn" "source": "components/Stepper.wrn"
}, },
{ {
+114 -9
View File
@@ -1,17 +1,122 @@
// CustomScrollbar -- a scrolling region with a themed scrollbar.
//
// <CustomScrollbar maxHeight="20rem">...long content...</CustomScrollbar>
//
// Scrollbar appearance is CSS, not script: scrollbar-width and scrollbar-color
// are the standard properties, and the ::-webkit-scrollbar rules cover the
// browsers that still need them.
//
// This component used to declare a scroll output it never emitted, and its
// props were columns, gap and maxWidth copied from a grid scaffold. The output
// is removed rather than left unimplemented -- a caller can listen for a plain
// scroll event on the element, which is what it would have been anyway.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component CustomScrollbar { component CustomScrollbar {
outputs {
scroll(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props { props {
size: string = "default"
color: string = "primary" color: string = "primary"
columns: number = 2 size: string = "default"
gap: string = "md" axis: string = "vertical"
maxWidth: string = "xl" // Track thickness in pixels. Clamped, because it arrives as an attribute
// and a scrollbar wider than the content is not useful to anyone.
thickness: number = 8
maxHeight: string = "20rem"
radius: string = "999px"
class: string = "" class: string = ""
} }
functions {
shared function trackSize() {
var value = Number(thickness)
if (!value || value < 2) {
return 8
}
return Math.min(24, value)
}
}
view { view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--custom-scrollbar wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div> <div
{...attrs}
data-ui-component="CustomScrollbar"
class='wire-scrollbar {class}'
data-color='{color}'
data-size='{size}'
data-axis='{axis}'
style='--scrollbar-thickness:{trackSize()}px;--scrollbar-radius:{radius};max-height:{maxHeight};'
>
<slot />
</div>
}
style {
.wire-scrollbar {
--scrollbar-accent: var(--wire-color-primary);
min-width: 0;
overflow: auto;
/*
* The standard properties. Firefox and current Chrome honour these; the
* webkit rules below are only for the engines that still ignore them.
*/
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--scrollbar-accent) 55%, transparent) transparent;
overscroll-behavior: contain;
}
.wire-scrollbar[data-color="secondary"] {
--scrollbar-accent: var(--wire-color-secondary);
}
.wire-scrollbar[data-color="success"] {
--scrollbar-accent: var(--wire-color-success);
}
.wire-scrollbar[data-color="danger"] {
--scrollbar-accent: var(--wire-color-danger);
}
.wire-scrollbar[data-color="info"] {
--scrollbar-accent: var(--wire-color-info);
}
.wire-scrollbar[data-axis="vertical"] {
overflow-x: hidden;
overflow-y: auto;
}
.wire-scrollbar[data-axis="horizontal"] {
overflow-x: auto;
overflow-y: hidden;
}
.wire-scrollbar::-webkit-scrollbar {
width: var(--scrollbar-thickness, 8px);
height: var(--scrollbar-thickness, 8px);
}
.wire-scrollbar::-webkit-scrollbar-track {
background: var(--wire-color-surface-soft);
border-radius: var(--scrollbar-radius, 999px);
}
.wire-scrollbar::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--scrollbar-accent) 45%, transparent);
border-radius: var(--scrollbar-radius, 999px);
}
.wire-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-accent);
}
/*
* A pointer-less device paints its own overlay scrollbar and ignores the
* width above, so the region keeps its own padding instead.
*/
@media (hover: none) {
.wire-scrollbar {
scrollbar-width: auto;
}
}
} }
} }
+199 -8
View File
@@ -1,19 +1,210 @@
// LayoutSplitter -- two panes with a divider the reader can move.
//
// <LayoutSplitter size={40} minSize={20}>
// <div data-slot="start">...</div>
// <div data-slot="end">...</div>
// </LayoutSplitter>
//
// The dragging lives in the reactive runtime behind data-wrn-splitter. Pointer
// moves fire far too often to route through a client function, and a state
// write made inside a pointermove callback is dropped, so the resolved size is
// held in the DOM as the --wrn-split custom property and these styles read it.
//
// This component previously declared resizeStart, resize and resizeEnd with no
// pointer handling whatsoever: a caller wired up @resize and received nothing,
// for ever, with no error.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component LayoutSplitter { component LayoutSplitter {
outputs { outputs {
resizeStart(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]) resize(payload: { size: number })
resize(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
resizeEnd(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
} }
props { props {
size: string = "default"
color: string = "primary" color: string = "primary"
columns: number = 2 size: number = 50
gap: string = "md" orientation: string = "horizontal"
maxWidth: string = "xl" // Smallest share either pane may take, as a percentage. It also fixes the
// upper bound at 100 - minSize, so neither pane can be dragged away to
// nothing and left unrecoverable by pointer.
minSize: number = 15
step: number = 5
label: string = "Resize panels"
class: string = "" class: string = ""
} }
functions {
shared function isVertical() {
return orientation === "vertical"
}
shared function lowerBound() {
var value = Number(minSize)
if (!value || value < 0) {
return 15
}
return Math.min(45, value)
}
shared function upperBound() {
return 100 - lowerBound()
}
// Sizes arrive as HTML attributes, so a value outside the bounds is
// routine rather than exceptional. Clamp instead of rendering something
// the reader cannot undo.
shared function currentSize() {
var value = Number(size)
if (!value || value < 0) {
return 50
}
return Math.min(upperBound(), Math.max(lowerBound(), value))
}
shared function stepSize() {
var value = Number(step)
return value && value > 0 ? value : 5
}
}
view { view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--layout-splitter wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div> <div
{...attrs}
data-ui-component="LayoutSplitter"
class='wire-splitter {class}'
data-color='{color}'
data-orientation='{orientation}'
data-wrn-splitter='{isVertical() ? "vertical" : "horizontal"}'
data-wrn-splitter-min='{lowerBound()}'
data-wrn-splitter-step='{stepSize()}'
style='--wrn-split:{currentSize()}%;'
>
<div class="wire-splitter__pane wire-splitter__pane--start">
<slot name="start"></slot>
</div>
<div
class="wire-splitter__handle"
data-wrn-splitter-handle="true"
role="separator"
tabindex="0"
aria-label='{label}'
aria-orientation='{isVertical() ? "horizontal" : "vertical"}'
aria-valuenow='{currentSize()}'
aria-valuemin='{lowerBound()}'
aria-valuemax='{upperBound()}'
>
<span class="wire-splitter__grip" aria-hidden="true"></span>
</div>
<div class="wire-splitter__pane wire-splitter__pane--end">
<slot name="end"></slot>
</div>
<slot />
</div>
}
style {
.wire-splitter {
--splitter-accent: var(--wire-color-primary);
display: grid;
/* The first track follows the size the runtime resolves. */
grid-template-columns: var(--wrn-split, 50%) auto minmax(0, 1fr);
align-items: stretch;
width: 100%;
min-width: 0;
}
.wire-splitter[data-color="secondary"] {
--splitter-accent: var(--wire-color-secondary);
}
.wire-splitter[data-color="success"] {
--splitter-accent: var(--wire-color-success);
}
.wire-splitter[data-color="danger"] {
--splitter-accent: var(--wire-color-danger);
}
.wire-splitter[data-color="info"] {
--splitter-accent: var(--wire-color-info);
}
.wire-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--wrn-split, 50%) auto minmax(0, 1fr);
}
.wire-splitter__pane {
min-width: 0;
min-height: 0;
overflow: auto;
}
.wire-splitter__handle {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0 0.25rem;
border: 0;
background: transparent;
cursor: col-resize;
/* Without this the pointer drag selects the text in both panes. */
touch-action: none;
user-select: none;
}
.wire-splitter[data-orientation="vertical"] .wire-splitter__handle {
padding: 0.25rem 0;
cursor: row-resize;
}
.wire-splitter__grip {
display: block;
width: 2px;
height: 100%;
min-height: 1.5rem;
border-radius: 999px;
background: var(--wire-color-border);
transition: background 140ms ease;
}
.wire-splitter[data-orientation="vertical"] .wire-splitter__grip {
width: 100%;
min-width: 1.5rem;
height: 2px;
}
.wire-splitter__handle:hover .wire-splitter__grip,
.wire-splitter[data-wrn-splitter-dragging="true"] .wire-splitter__grip {
background: var(--splitter-accent);
}
.wire-splitter__handle:focus-visible {
outline: 2px solid var(--splitter-accent);
outline-offset: -2px;
border-radius: var(--wire-radius-sm);
}
/*
* Two panes side by side stop making sense on a phone. Stacking them keeps
* both readable, and the divider stops being draggable because the grid
* no longer has a second track to trade against.
*/
@media (max-width: 639px) {
.wire-splitter,
.wire-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
}
.wire-splitter__handle {
cursor: default;
}
}
} }
} }
+15
View File
@@ -214,6 +214,21 @@ component MegaMenu {
transform: rotate(90deg); transform: rotate(90deg);
} }
/*
* The panel is offset below the trigger for looks, which used to close the
* menu on the way to it: that offset is dead space belonging to neither
* element, so crossing it fired mouseleave on the root. The bridge below
* covers the gap with a descendant, so the pointer never actually leaves.
*/
.wire-mega__panel::before {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 100%;
height: 0.6rem;
}
.wire-mega__panel { .wire-mega__panel {
position: absolute; position: absolute;
z-index: 40; z-index: 40;
+195 -4
View File
@@ -14,6 +14,10 @@
component Stepper { component Stepper {
outputs { outputs {
change(payload: { index: number; step: object }) change(payload: { index: number; step: object })
back(payload: { index: number; step: object })
next(payload: { index: number; step: object })
skip(payload: { index: number; step: object })
finish(payload: { index: number; step: object })
} }
props { props {
@@ -24,6 +28,19 @@ component Stepper {
orientation: string = "horizontal" orientation: string = "horizontal"
clickable: boolean = false clickable: boolean = false
label: string = "Progress" label: string = "Progress"
// Render each step body, showing only the active one -- same contract as
// Tabs, so a wizard does not have to hand-roll the panel switching.
showPanel: boolean = false
controls: boolean = false
allowSkip: boolean = false
// Lets a form hold the step. The component never validates anything
// itself: the page owns the form, so it sets this while the step is
// incomplete and clears it once the step passes.
nextDisabled: boolean = false
backLabel: string = "Back"
nextLabel: string = "Next"
skipLabel: string = "Skip"
finishLabel: string = "Finish"
class: string = "" class: string = ""
} }
@@ -63,6 +80,48 @@ component Stepper {
return orientation === "vertical" ? "vertical" : "horizontal" return orientation === "vertical" ? "vertical" : "horizontal"
} }
shared function isActiveStep(index) {
return index === activeIndex()
}
shared function lastIndex() {
return Math.max(0, stepList().length - 1)
}
shared function onLastStep() {
return activeIndex() >= lastIndex()
}
shared function activeStep() {
var list = stepList()
return list.length ? list[activeIndex()] : {}
}
client function goBack() {
var target = Math.max(0, activeIndex() - 1)
output.back({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function goNext() {
if (nextDisabled) {
return
}
if (onLastStep()) {
output.finish({ index: activeIndex(), step: activeStep() })
return
}
var target = Math.min(lastIndex(), activeIndex() + 1)
output.next({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function goSkip() {
var target = Math.min(lastIndex(), activeIndex() + 1)
output.skip({ index: target, step: stepList()[target] || {} })
output.change({ index: target, step: stepList()[target] || {} })
}
client function selectStep(index, step) { client function selectStep(index, step) {
if (!clickable) { if (!clickable) {
return return
@@ -72,7 +131,7 @@ component Stepper {
} }
view { view {
<ol <div
{...attrs} {...attrs}
data-ui-component="Stepper" data-ui-component="Stepper"
class='wire-stepper {class}' class='wire-stepper {class}'
@@ -80,6 +139,9 @@ component Stepper {
data-color='{color}' data-color='{color}'
data-size='{size}' data-size='{size}'
data-clickable='{clickable}' data-clickable='{clickable}'
>
<ol
class="wire-stepper__list"
data-wrn-roving='{rovingAxis()}' data-wrn-roving='{rovingAxis()}'
aria-label='{label}' aria-label='{label}'
> >
@@ -111,13 +173,62 @@ component Stepper {
</span> </span>
</li> </li>
{/each} {/each}
<slot />
</ol> </ol>
<div class="wire-stepper__panels" data-show="showPanel">
{#each stepList() as step, index}
<div
class="wire-stepper__panel"
data-show='isActiveStep(index)'
aria-hidden='{index === activeIndex() ? "false" : "true"}'
>
<h4 class="wire-stepper__panel-title" data-show="step.title">{step.title}</h4>
<p class="wire-stepper__panel-body" data-show="step.content">{step.content}</p>
<slot name="panel-{index}"></slot>
</div>
{/each}
</div>
<div class="wire-stepper__controls" data-show="controls">
<button
type="button"
class="wire-stepper__button-control wire-stepper__back"
disabled='{activeIndex() === 0}'
@click='goBack()'
>
{backLabel}
</button>
<span class="wire-stepper__controls-spacer"></span>
<button
type="button"
class="wire-stepper__button-control wire-stepper__skip"
data-show="allowSkip && !onLastStep()"
@click='goSkip()'
>
{skipLabel}
</button>
<button
type="button"
class="wire-stepper__button-control wire-stepper__next"
data-primary="true"
disabled='{nextDisabled}'
@click='goNext()'
>
{onLastStep() ? finishLabel : nextLabel}
</button>
</div>
<slot />
</div>
} }
style { style {
.wire-stepper { .wire-stepper {
--stepper-accent: var(--wire-color-primary); --stepper-accent: var(--wire-color-primary);
max-width: 100%;
}
.wire-stepper__list {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
margin: 0; margin: 0;
@@ -150,7 +261,7 @@ component Stepper {
font-size: 1rem; font-size: 1rem;
} }
.wire-stepper[data-orientation="vertical"] { .wire-stepper[data-orientation="vertical"] .wire-stepper__list {
flex-direction: column; flex-direction: column;
} }
@@ -241,9 +352,89 @@ component Stepper {
color: var(--wire-color-text-muted); color: var(--wire-color-text-muted);
} }
.wire-stepper__panels {
margin-top: 1rem;
}
.wire-stepper__panel {
padding: 1rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface);
animation: wire-stepper-in 200ms ease both;
}
.wire-stepper__panel-title {
margin: 0 0 0.35rem;
font-size: 0.95rem;
font-weight: 700;
}
.wire-stepper__panel-body {
margin: 0;
color: var(--wire-color-text-muted);
line-height: 1.6;
}
.wire-stepper__controls {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.85rem;
}
.wire-stepper__controls-spacer {
flex: 1 1 auto;
}
.wire-stepper__button-control {
appearance: none;
padding: 0.45rem 0.9rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface);
color: var(--wire-color-text);
font: inherit;
font-size: 0.86rem;
font-weight: 600;
cursor: pointer;
}
.wire-stepper__button-control:hover:not(:disabled) {
background: var(--wire-color-surface-soft);
}
.wire-stepper__button-control:focus-visible {
outline: 2px solid var(--stepper-accent);
outline-offset: 2px;
}
.wire-stepper__button-control[data-primary="true"] {
border-color: var(--stepper-accent);
background: var(--stepper-accent);
color: var(--wire-color-primary-contrast);
}
/* A held step has to look held, or the button reads as broken. */
.wire-stepper__button-control:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@keyframes wire-stepper-in {
from {
opacity: 0;
transform: translateX(0.75rem);
}
to {
opacity: 1;
transform: none;
}
}
/* A horizontal stepper cannot stay side by side on a phone. */ /* A horizontal stepper cannot stay side by side on a phone. */
@media (max-width: 639px) { @media (max-width: 639px) {
.wire-stepper { .wire-stepper__list {
flex-direction: column; flex-direction: column;
} }
} }
+35 -3
View File
@@ -28,6 +28,9 @@ component Tabs {
} }
state activeValue = "" state activeValue = ""
// Which way the panel should slide in. Set on every change so the animation
// follows the direction of travel rather than always coming from one side.
state slideFrom = "forward"
functions { functions {
shared function itemList() { shared function itemList() {
@@ -80,6 +83,14 @@ component Tabs {
return return
} }
var value = valueOf(item, index) var value = valueOf(item, index)
var list = itemList()
var previous = -1
for (var scan = 0; scan < list.length; scan += 1) {
if (valueOf(list[scan], scan) === currentValue()) {
previous = scan
}
}
slideFrom = previous > index ? "back" : "forward"
activeValue = value activeValue = value
if (mode === "url" && window.history && window.history.pushState) { if (mode === "url" && window.history && window.history.pushState) {
@@ -101,6 +112,7 @@ component Tabs {
data-orientation='{orientation}' data-orientation='{orientation}'
data-color='{color}' data-color='{color}'
data-size='{size}' data-size='{size}'
data-slide='{slideFrom}'
data-mode='{mode}' data-mode='{mode}'
data-param='{param}' data-param='{param}'
> >
@@ -272,7 +284,16 @@ component Tabs {
.wire-tabs__panel { .wire-tabs__panel {
outline: none; outline: none;
animation: wire-tabs-in 200ms ease both; animation: wire-tabs-slide-forward 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.wire-tabs[data-slide="back"] .wire-tabs__panel {
animation-name: wire-tabs-slide-back;
}
/* The panels clip their own slide so it never widens the page. */
.wire-tabs__panels {
overflow-x: clip;
} }
.wire-tabs__panel:focus-visible { .wire-tabs__panel:focus-visible {
@@ -297,10 +318,21 @@ component Tabs {
margin-top: 0.6rem; margin-top: 0.6rem;
} }
@keyframes wire-tabs-in { @keyframes wire-tabs-slide-forward {
from { from {
opacity: 0; opacity: 0;
transform: translateY(4px); transform: translateX(1.25rem);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes wire-tabs-slide-back {
from {
opacity: 0;
transform: translateX(-1.25rem);
} }
to { to {
opacity: 1; opacity: 1;
+145 -1
View File
@@ -2522,7 +2522,10 @@ test("stepper marks complete, current and upcoming steps", async () => {
expect(items[1]!.getAttribute("data-status")).toBe("current"); expect(items[1]!.getAttribute("data-status")).toBe("current");
expect(items[2]!.getAttribute("data-status")).toBe("upcoming"); expect(items[2]!.getAttribute("data-status")).toBe("upcoming");
expect(items[1]!.getAttribute("aria-current")).toBe("step"); expect(items[1]!.getAttribute("aria-current")).toBe("step");
expect(dom.querySelector(".wire-stepper")!.tagName.toLowerCase()).toBe("ol"); // One root wraps the list, the panels and the controls; the steps
// themselves are still an ordered list, which is what carries the semantics.
expect(dom.querySelector(".wire-stepper")!.tagName.toLowerCase()).toBe("div");
expect(dom.querySelector(".wire-stepper__list")!.tagName.toLowerCase()).toBe("ol");
}); });
test("stepper renders vertically and clamps an out-of-range active index", async () => { test("stepper renders vertically and clamps an out-of-range active index", async () => {
@@ -2875,3 +2878,144 @@ test("breadcrumb marks the trail end without emitting an empty aria-current", as
expect(dom.querySelector(".wire-breadcrumb")!.getAttribute("aria-label")).toBeTruthy(); expect(dom.querySelector(".wire-breadcrumb")!.getAttribute("aria-label")).toBeTruthy();
expect(dom.querySelectorAll(".wire-breadcrumb__item").length).toBeGreaterThanOrEqual(3); expect(dom.querySelectorAll(".wire-breadcrumb__item").length).toBeGreaterThanOrEqual(3);
}); });
test("mega menu bridges the gap between its trigger and panel", async () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
/*
* The panel is offset below the trigger, and that offset is dead space
* belonging to neither element: travelling to the panel fired mouseleave on
* the root and closed the menu before the pointer arrived. A descendant
* covering the gap keeps the pointer inside the component.
*/
expect(source).toContain(".wire-mega__panel::before");
expect(source).toMatch(/\.wire-mega__panel::before \{[^}]*bottom: 100%/s);
});
test("stepper shows only the active step content and offers back, next and skip", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [
{ label: "Account", content: "ACCOUNT BODY" },
{ label: "Billing", content: "BILLING BODY" },
{ label: "Confirm", content: "CONFIRM BODY" },
],
active: 1,
showPanel: true,
controls: true,
allowSkip: true,
});
const dom = mountHtml(html);
const panels = [...dom.querySelectorAll(".wire-stepper__panel")];
expect(panels).toHaveLength(3);
/*
* Asserted through aria-hidden rather than data-show. data-show is emitted
* verbatim for the client runtime to evaluate, so its server-rendered value
* is not a reliable statement about which step is showing; aria-hidden is
* interpolated at render and says exactly that.
*/
expect(panels.map((p) => p.getAttribute("aria-hidden"))).toEqual(["true", "false", "true"]);
expect(dom.querySelector(".wire-stepper__back")).not.toBeNull();
expect(dom.querySelector(".wire-stepper__next")).not.toBeNull();
expect(dom.querySelector(".wire-stepper__skip")).not.toBeNull();
});
test("stepper back is disabled on the first step and next becomes finish on the last", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const steps = [{ label: "One" }, { label: "Two" }];
const first = mountHtml(await renderComponent(source, { steps, active: 0, controls: true }));
expect(first.querySelector(".wire-stepper__back")!.getAttribute("disabled")).not.toBeNull();
expect(first.querySelector(".wire-stepper__next")!.textContent).toContain("Next");
const last = mountHtml(await renderComponent(source, { steps, active: 1, controls: true }));
expect(last.querySelector(".wire-stepper__next")!.textContent).toContain("Finish");
});
test("stepper next can be gated so a form can hold it until the step validates", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [{ label: "One" }, { label: "Two" }],
active: 0,
controls: true,
nextDisabled: true,
});
const dom = mountHtml(html);
expect(dom.querySelector(".wire-stepper__next")!.getAttribute("disabled")).not.toBeNull();
});
test("layout splitter renders two panes and an operable separator", async () => {
const source = readFileSync(uiComponentPath("LayoutSplitter"), "utf8");
/*
* It used to declare resizeStart, resize and resizeEnd with no pointer
* handling at all, so a caller wired up @resize and received nothing for
* ever. The behaviour now lives in the runtime behind these markers.
*/
expect(source).toContain("data-wrn-splitter");
expect(source).toContain("data-wrn-splitter-handle");
const html = await renderComponent(source, {
orientation: "horizontal",
size: 40,
minSize: 20,
label: "Resize panels",
});
const dom = mountHtml(html);
const root = dom.querySelector(".wire-splitter") as HTMLElement;
expect(root.getAttribute("data-wrn-splitter")).toBe("horizontal");
expect(root.getAttribute("style")).toContain("--wrn-split");
const handle = dom.querySelector("[data-wrn-splitter-handle]") as HTMLElement;
expect(handle.getAttribute("role")).toBe("separator");
expect(handle.getAttribute("tabindex")).toBe("0");
expect(handle.getAttribute("aria-valuenow")).toBe("40");
expect(handle.getAttribute("aria-valuemin")).toBe("20");
expect(handle.getAttribute("aria-valuemax")).toBe("80");
expect(handle.getAttribute("aria-orientation")).toBe("vertical");
expect(dom.querySelectorAll(".wire-splitter__pane")).toHaveLength(2);
});
test("layout splitter clamps an out-of-range size and flips orientation", async () => {
const source = readFileSync(uiComponentPath("LayoutSplitter"), "utf8");
const dom = mountHtml(
await renderComponent(source, { orientation: "vertical", size: 95, minSize: 25 }),
);
const handle = dom.querySelector("[data-wrn-splitter-handle]") as HTMLElement;
// 95 is past the 75 bound implied by minSize, so it clamps rather than
// leaving one pane unrecoverable.
expect(handle.getAttribute("aria-valuenow")).toBe("75");
// A vertical splitter stacks, so its separator is a horizontal bar.
expect(handle.getAttribute("aria-orientation")).toBe("horizontal");
});
test("custom scrollbar styles the scrollbar and drops its fake output", async () => {
const source = readFileSync(uiComponentPath("CustomScrollbar"), "utf8");
/*
* It declared a scroll output it never emitted, and its props were columns,
* gap and maxWidth copied from a grid scaffold. A caller can listen for a
* plain scroll event on the element, so the output is gone rather than left
* unimplemented.
*/
expect(source).not.toContain("outputs {");
// Matched as a declaration: the word still appears in the comment recording
// why those props were wrong.
expect(source).not.toMatch(/^\s*columns:/m);
expect(source).not.toMatch(/^\s*gap:/m);
expect(source).toContain("scrollbar-color");
expect(source).toContain("::-webkit-scrollbar");
const html = await renderComponent(source, {
axis: "vertical",
thickness: 10,
maxHeight: "18rem",
});
const dom = mountHtml(html);
const root = dom.querySelector(".wire-scrollbar") as HTMLElement;
expect(root.getAttribute("data-axis")).toBe("vertical");
const style = root.getAttribute("style") || "";
expect(style).toContain("--scrollbar-thickness");
expect(style).toContain("18rem");
});
-920
View File
File diff suppressed because it is too large Load Diff