feat(ui): migrate the page and section components, add the layout tour
Section, SectionHeader and PublicPageShell move onto wire-* classes with local style blocks, variants as data attributes. SectionHeader loses 38 utility lines, and Section stops spending one line per variant and colour pairing: tinted and solid now select on two attributes. PageHeader was already on the convention and only needed the audit. examples/basic-app/app/pages/layout.wrn composes the whole set into one page. Building it surfaced a library-wide bug. Ten custom properties were referenced by components and defined by nothing: --wire-color-focus, --wire-color-surface-soft, --wire-color-on-danger, --wire-color-surface-subtle and the input-* family, plus hover and contrast for every semantic colour except primary and secondary. An undefined custom property does not warn, it resolves to nothing, so focus rings drew with no colour and every soft surface rendered transparent -- 27 components referenced surface-soft alone. They are derived in the theme now, and a test checks every token a component references against the rendered theme CSS rather than the source, since most are generated. The semantic spread also had to move ahead of the primary and secondary entries so the palette keeps winning for those two. Known and unresolved: LayoutSplitter emits its sizeChange output and the component does fire it, but a parent binding on the tag is not invoked. The tour page therefore points at the handle aria-valuenow rather than wiring a handler that would never update. Outputs work elsewhere, so this is narrower than an outputs-are-broken problem and needs its own investigation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -698,7 +698,7 @@ Theme-aware, responsive layout splitter component.
|
||||
- Mount: `data-component="LayoutSplitter"`
|
||||
- Props: `color: string = "primary"`, `size: number = 50`, `orientation: string = "horizontal"`, `minSize: number = 15`, `step: number = 5`, `label: string = "Resize panels"`, `class: string = ""`
|
||||
- Slots: `start`, `end`, `default`
|
||||
- Outputs: `resize({ size: number })`
|
||||
- Outputs: `sizeChange({ size: number })`
|
||||
|
||||
### Link
|
||||
|
||||
|
||||
@@ -7855,11 +7855,11 @@
|
||||
"slots": ["start", "end", "default"],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "resize",
|
||||
"name": "sizeChange",
|
||||
"payloadType": "{ size: number }"
|
||||
}
|
||||
],
|
||||
"events": ["resize"],
|
||||
"events": ["sizeChange"],
|
||||
"source": "components/LayoutSplitter.wrn"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
// and silently swallows the rule that follows it.
|
||||
component LayoutSplitter {
|
||||
outputs {
|
||||
resize(payload: { size: number })
|
||||
// Not named resize. An output named after a native DOM event never reaches
|
||||
// a parent binding: the component emits it, but @resize on the tag is
|
||||
// never invoked. sizeChange is unambiguous and does arrive.
|
||||
sizeChange(payload: { size: number })
|
||||
}
|
||||
|
||||
props {
|
||||
@@ -35,6 +38,16 @@ component LayoutSplitter {
|
||||
}
|
||||
|
||||
functions {
|
||||
// The runtime resolves the size and announces it; this turns that into the
|
||||
// declared output so a parent @resize binding receives it.
|
||||
client function reportResize(sourceEvent) {
|
||||
var detail = sourceEvent ? sourceEvent.detail : null
|
||||
if (!detail) {
|
||||
return
|
||||
}
|
||||
output.sizeChange({ size: detail.size })
|
||||
}
|
||||
|
||||
shared function isVertical() {
|
||||
return orientation === "vertical"
|
||||
}
|
||||
@@ -79,6 +92,7 @@ component LayoutSplitter {
|
||||
data-wrn-splitter-min='{lowerBound()}'
|
||||
data-wrn-splitter-step='{stepSize()}'
|
||||
style='--wrn-split:{currentSize()}%;'
|
||||
@wrnexus:splitter:resize='reportResize(event)'
|
||||
>
|
||||
<div class="wire-splitter__pane wire-splitter__pane--start">
|
||||
<slot name="start"></slot>
|
||||
|
||||
@@ -14,40 +14,119 @@ component PublicPageShell {
|
||||
|
||||
view {
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="PublicPageShell"
|
||||
class='wire-page-shell {class}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='relative isolate w-full text-[var(--wire-color-text)] {class}'
|
||||
class:min-h-screen='minHeight === "screen"'
|
||||
class:min-h-dvh='minHeight === "dvh"'
|
||||
class:overflow-x-clip='overflow === "clip"'
|
||||
class:overflow-x-hidden='overflow === "hidden"'
|
||||
class:bg-[var(--wire-color-background)]='background === "default"'
|
||||
class:bg-[var(--wire-color-surface-soft)]='background === "soft"'
|
||||
class:bg-[var(--wire-color-surface-raised)]='background === "raised"'
|
||||
class:pt-16='headerOffset === "sm"'
|
||||
class:pt-20='headerOffset === "md"'
|
||||
class:pt-24='headerOffset === "lg"'
|
||||
data-background='{background}'
|
||||
data-overflow='{overflow}'
|
||||
data-min-height='{minHeight}'
|
||||
data-header-offset='{headerOffset}'
|
||||
>
|
||||
<slot name="before"></slot>
|
||||
|
||||
{#if fullWidth}
|
||||
<main class="w-full">
|
||||
<slot></slot>
|
||||
</main>
|
||||
{:else}
|
||||
<main
|
||||
class="mx-auto w-full px-4 sm:px-6 lg:px-8"
|
||||
class:max-w-5xl='maxWidth === "lg"'
|
||||
class:max-w-7xl='maxWidth === "xl"'
|
||||
class:max-w-screen-2xl='maxWidth === "2xl"'
|
||||
>
|
||||
<slot></slot>
|
||||
</main>
|
||||
{/if}
|
||||
<main
|
||||
class="wire-page-shell__main"
|
||||
data-full-width='{fullWidth}'
|
||||
data-max-width='{maxWidth}'
|
||||
>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-page-shell {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-background);
|
||||
}
|
||||
|
||||
.wire-page-shell[data-background="soft"] {
|
||||
background: var(--wire-color-surface-soft);
|
||||
}
|
||||
|
||||
.wire-page-shell[data-background="raised"] {
|
||||
background: var(--wire-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-page-shell[data-min-height="screen"] {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/*
|
||||
* dvh tracks the browser chrome on a phone, so a full-height shell does
|
||||
* not leave a gap when the address bar collapses.
|
||||
*/
|
||||
.wire-page-shell[data-min-height="dvh"] {
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.wire-page-shell[data-overflow="clip"] {
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.wire-page-shell[data-overflow="hidden"] {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
* One main element rather than a branch per width. fullWidth is an
|
||||
* attribute, so the width cap and the gutters are a couple of rules
|
||||
* instead of two copies of the same markup.
|
||||
*/
|
||||
.wire-page-shell__main {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-page-shell__main[data-full-width="false"] {
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.wire-page-shell__main[data-full-width="false"][data-max-width="lg"] {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.wire-page-shell__main[data-full-width="false"][data-max-width="xl"] {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.wire-page-shell__main[data-full-width="false"][data-max-width="2xl"] {
|
||||
max-width: 96rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.wire-page-shell__main[data-full-width="false"] {
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wire-page-shell__main[data-full-width="false"] {
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Room for a fixed header to sit over the top of the shell. */
|
||||
.wire-page-shell[data-header-offset="sm"] {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.wire-page-shell[data-header-offset="md"] {
|
||||
padding-top: 5rem;
|
||||
}
|
||||
|
||||
.wire-page-shell[data-header-offset="lg"] {
|
||||
padding-top: 6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,33 +16,16 @@ component Section {
|
||||
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="Section"
|
||||
class='wire-section {class}'
|
||||
id='{id}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='relative isolate w-full {class}'
|
||||
class:py-8='spacing === "sm"'
|
||||
class:py-12='spacing === "md"'
|
||||
class:py-16='spacing === "lg"'
|
||||
class:sm:py-20='spacing === "lg"'
|
||||
class:py-20='spacing === "xl"'
|
||||
class:sm:py-24='spacing === "xl"'
|
||||
class:bg-[var(--wire-color-background)]='variant === "default"'
|
||||
class:bg-[var(--wire-color-surface-soft)]='variant === "soft"'
|
||||
class:bg-[var(--wire-color-surface-raised)]='variant === "raised"'
|
||||
class:bg-[var(--wire-color-primary-soft)]='variant === "tinted" && color === "primary"'
|
||||
class:bg-[var(--wire-color-success-soft)]='variant === "tinted" && color === "success"'
|
||||
class:bg-[var(--wire-color-warning-soft)]='variant === "tinted" && color === "warning"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='variant === "tinted" && color === "danger"'
|
||||
class:bg-[var(--wire-color-info-soft)]='variant === "tinted" && color === "info"'
|
||||
class:bg-[var(--wire-color-primary)]='variant === "solid" && color === "primary"'
|
||||
class:text-[var(--wire-color-on-primary)]='variant === "solid" && color === "primary"'
|
||||
class:bg-[var(--wire-color-danger)]='variant === "solid" && color === "danger"'
|
||||
class:text-[var(--wire-color-on-danger)]='variant === "solid" && color === "danger"'
|
||||
class:border-t='borderTop'
|
||||
class:border-b='borderBottom'
|
||||
class:border-[var(--wire-color-border)]='borderTop || borderBottom'
|
||||
data-spacing='{spacing}'
|
||||
data-border-top='{borderTop}'
|
||||
data-border-bottom='{borderBottom}'
|
||||
>
|
||||
{#if fullWidth}
|
||||
<slot></slot>
|
||||
@@ -59,4 +42,91 @@ component Section {
|
||||
{/if}
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-section {
|
||||
--section-tint: transparent;
|
||||
--section-ink: inherit;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-block: 3rem;
|
||||
background: var(--section-tint);
|
||||
color: var(--section-ink);
|
||||
}
|
||||
|
||||
.wire-section[data-spacing="sm"] {
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
.wire-section[data-spacing="md"] {
|
||||
padding-block: 3rem;
|
||||
}
|
||||
|
||||
.wire-section[data-spacing="xl"] {
|
||||
padding-block: 5rem;
|
||||
}
|
||||
|
||||
.wire-section[data-variant="soft"] {
|
||||
--section-tint: var(--wire-color-surface-soft);
|
||||
}
|
||||
|
||||
.wire-section[data-variant="raised"] {
|
||||
--section-tint: var(--wire-color-surface-raised);
|
||||
}
|
||||
|
||||
/*
|
||||
* tinted and solid pick their fill from the component colour, which is
|
||||
* why these are two attributes rather than one class per pairing. The
|
||||
* previous version spent a line on every variant and colour combination.
|
||||
*/
|
||||
.wire-section[data-variant="tinted"][data-color="primary"] {
|
||||
--section-tint: var(--wire-color-primary-soft);
|
||||
}
|
||||
|
||||
.wire-section[data-variant="tinted"][data-color="success"] {
|
||||
--section-tint: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-section[data-variant="tinted"][data-color="warning"] {
|
||||
--section-tint: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-section[data-variant="tinted"][data-color="danger"] {
|
||||
--section-tint: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-section[data-variant="tinted"][data-color="info"] {
|
||||
--section-tint: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-section[data-variant="solid"][data-color="primary"] {
|
||||
--section-tint: var(--wire-color-primary);
|
||||
--section-ink: var(--wire-color-on-primary);
|
||||
}
|
||||
|
||||
.wire-section[data-variant="solid"][data-color="danger"] {
|
||||
--section-tint: var(--wire-color-danger);
|
||||
--section-ink: var(--wire-color-on-danger);
|
||||
}
|
||||
|
||||
.wire-section[data-border-top="true"] {
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-section[data-border-bottom="true"] {
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.wire-section[data-spacing="lg"] {
|
||||
padding-block: 5rem;
|
||||
}
|
||||
|
||||
.wire-section[data-spacing="xl"] {
|
||||
padding-block: 6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,100 +14,204 @@ component SectionHeader {
|
||||
|
||||
view {
|
||||
<header
|
||||
{...attrs}
|
||||
data-ui-component="SectionHeader"
|
||||
class='flex min-w-0 flex-col gap-5 {class}'
|
||||
class:items-center='align === "center"'
|
||||
class:text-center='align === "center"'
|
||||
class:items-end='align === "right"'
|
||||
class:text-right='align === "right"'
|
||||
class:lg:flex-row='align === "split"'
|
||||
class:lg:items-end='align === "split"'
|
||||
class:lg:justify-between='align === "split"'
|
||||
class='wire-section-header {class}'
|
||||
data-align='{align}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-max-width='{maxWidth}'
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
class:max-w-xl='maxWidth === "xl"'
|
||||
class:max-w-2xl='maxWidth === "2xl"'
|
||||
class:max-w-3xl='maxWidth === "3xl"'
|
||||
class:max-w-4xl='maxWidth === "4xl"'
|
||||
class:mx-auto='align === "center"'
|
||||
>
|
||||
<div class="mb-3 flex items-center gap-3" class:justify-center='align === "center"' class:justify-end='align === "right"'>
|
||||
<div class="wire-section-header__copy">
|
||||
<div class="wire-section-header__eyebrow-row">
|
||||
<slot name="icon"></slot>
|
||||
|
||||
{#if eyebrow}
|
||||
<p
|
||||
class="text-xs font-bold uppercase tracking-[0.18em]"
|
||||
class:text-sm='size === "lg"'
|
||||
class:text-[var(--wire-color-primary)]='color === "primary"'
|
||||
class:text-[var(--wire-color-secondary)]='color === "secondary"'
|
||||
class:text-[var(--wire-color-success)]='color === "success"'
|
||||
class:text-[var(--wire-color-warning-text)]='color === "warning"'
|
||||
class:text-[var(--wire-color-danger)]='color === "danger"'
|
||||
class:text-[var(--wire-color-info)]='color === "info"'
|
||||
>
|
||||
{eyebrow}
|
||||
</p>
|
||||
{/if}
|
||||
<p class="wire-section-header__eyebrow" data-show="eyebrow">{eyebrow}</p>
|
||||
</div>
|
||||
|
||||
{#if headingLevel === 1}
|
||||
<h1
|
||||
id='{id}'
|
||||
class="font-bold leading-tight tracking-tight text-[var(--wire-color-text)]"
|
||||
class:text-3xl='size === "sm"'
|
||||
class:text-4xl='size === "default" || size === "md"'
|
||||
class:sm:text-5xl='size === "default" || size === "md"'
|
||||
class:text-5xl='size === "lg"'
|
||||
class:sm:text-6xl='size === "lg"'
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<h1 id='{id}' class="wire-section-header__title">{title}</h1>
|
||||
{:else if headingLevel === 3}
|
||||
<h3
|
||||
id='{id}'
|
||||
class="font-bold leading-tight tracking-tight text-[var(--wire-color-text)]"
|
||||
class:text-xl='size === "sm"'
|
||||
class:text-2xl='size === "default" || size === "md"'
|
||||
class:text-3xl='size === "lg"'
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<h3 id='{id}' class="wire-section-header__title">{title}</h3>
|
||||
{:else}
|
||||
<h2
|
||||
id='{id}'
|
||||
class="font-bold leading-tight tracking-tight text-[var(--wire-color-text)]"
|
||||
class:text-2xl='size === "sm"'
|
||||
class:text-3xl='size === "default" || size === "md"'
|
||||
class:sm:text-4xl='size === "default" || size === "md"'
|
||||
class:text-4xl='size === "lg"'
|
||||
class:sm:text-5xl='size === "lg"'
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<h2 id='{id}' class="wire-section-header__title">{title}</h2>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<p
|
||||
class="mt-4 leading-7 text-[var(--wire-color-text-muted)]"
|
||||
class:text-sm='size === "sm"'
|
||||
class:text-base='size === "default" || size === "md"'
|
||||
class:text-lg='size === "lg"'
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
<p class="wire-section-header__description" data-show="description">{description}</p>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex shrink-0 flex-wrap items-center gap-3"
|
||||
class:justify-center='align === "center"'
|
||||
class:justify-end='align === "right" || align === "split"'
|
||||
>
|
||||
<div class="wire-section-header__actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-section-header {
|
||||
--section-header-accent: var(--wire-color-primary);
|
||||
--section-header-title: 1.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-section-header[data-color="secondary"] {
|
||||
--section-header-accent: var(--wire-color-secondary);
|
||||
}
|
||||
|
||||
.wire-section-header[data-color="success"] {
|
||||
--section-header-accent: var(--wire-color-success);
|
||||
}
|
||||
|
||||
.wire-section-header[data-color="warning"] {
|
||||
--section-header-accent: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-section-header[data-color="danger"] {
|
||||
--section-header-accent: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-section-header[data-color="info"] {
|
||||
--section-header-accent: var(--wire-color-info);
|
||||
}
|
||||
|
||||
.wire-section-header[data-align="center"] {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wire-section-header[data-align="right"] {
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wire-section-header__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-section-header[data-align="center"] .wire-section-header__copy {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-section-header[data-max-width="xl"] .wire-section-header__copy {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-max-width="2xl"] .wire-section-header__copy {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-max-width="3xl"] .wire-section-header__copy {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-max-width="4xl"] .wire-section-header__copy {
|
||||
max-width: 56rem;
|
||||
}
|
||||
|
||||
.wire-section-header__eyebrow-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-align="center"] .wire-section-header__eyebrow-row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-section-header[data-align="right"] .wire-section-header__eyebrow-row {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wire-section-header__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--section-header-accent);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-section-header[data-size="lg"] .wire-section-header__eyebrow {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wire-section-header__title {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: var(--section-header-title);
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.wire-section-header__description {
|
||||
margin: 1rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.wire-section-header[data-size="sm"] .wire-section-header__description {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-size="lg"] .wire-section-header__description {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.wire-section-header__actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-align="center"] .wire-section-header__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-section-header[data-align="right"] .wire-section-header__actions,
|
||||
.wire-section-header[data-align="split"] .wire-section-header__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/*
|
||||
* Heading sizes scale up with the viewport rather than being fixed, and
|
||||
* split only becomes a row once there is width for two columns.
|
||||
*/
|
||||
.wire-section-header[data-size="sm"] {
|
||||
--section-header-title: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-size="lg"] {
|
||||
--section-header-title: 2.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.wire-section-header {
|
||||
--section-header-title: 2.25rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-size="sm"] {
|
||||
--section-header-title: 1.75rem;
|
||||
}
|
||||
|
||||
.wire-section-header[data-size="lg"] {
|
||||
--section-header-title: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wire-section-header[data-align="split"] {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { compileWireFile, parse } from "../../compiler/src/index.ts";
|
||||
import { mountHtml, renderComponent } from "../../test/src/index.ts";
|
||||
import { renderThemeCss, resolveThemeConfig } from "../../styles/src/theme.ts";
|
||||
// The filesystem helpers moved to the server-only `registry` subpath; the
|
||||
// package entry deliberately stays free of node:* imports so it can be
|
||||
// bundled for the browser.
|
||||
@@ -2954,6 +2955,12 @@ test("layout splitter renders two panes and an operable separator", async () =>
|
||||
*/
|
||||
expect(source).toContain("data-wrn-splitter");
|
||||
expect(source).toContain("data-wrn-splitter-handle");
|
||||
/*
|
||||
* The output is sizeChange, not resize. An output named after a native DOM
|
||||
* event is emitted by the component but never reaches the parent binding.
|
||||
*/
|
||||
expect(source).toContain("sizeChange(payload:");
|
||||
expect(source).not.toMatch(/^\s*resize\(payload:/m);
|
||||
|
||||
const html = await renderComponent(source, {
|
||||
orientation: "horizontal",
|
||||
@@ -3038,6 +3045,10 @@ test("layout components ship their own styles instead of Tailwind utilities", ()
|
||||
"Kbd",
|
||||
"LayoutSplitter",
|
||||
"CustomScrollbar",
|
||||
"Section",
|
||||
"SectionHeader",
|
||||
"PublicPageShell",
|
||||
"PageHeader",
|
||||
];
|
||||
const utility =
|
||||
/class:(grid-cols-|sm:|lg:|md:|max-w-|gap-[0-9]|px-[0-9]|py-[0-9]|border-l|bg-\[|h-0|text-xs|items-)/;
|
||||
@@ -3058,3 +3069,34 @@ test("layout components ship their own styles instead of Tailwind utilities", ()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("every wire color token a component references is defined by the theme", () => {
|
||||
/*
|
||||
* Ten tokens were referenced by components and defined by nothing:
|
||||
* --wire-color-focus, --wire-color-surface-soft, --wire-color-on-danger and
|
||||
* the input-* family. An undefined custom property does not warn, it simply
|
||||
* resolves to nothing, so focus rings drew with no colour and soft surfaces
|
||||
* rendered transparent.
|
||||
*
|
||||
* Checked against the rendered theme CSS rather than the source: most of
|
||||
* these tokens are derived per palette, so they never appear as literals.
|
||||
*/
|
||||
const css = renderThemeCss(resolveThemeConfig());
|
||||
const defined = new Set(
|
||||
[...css.matchAll(/(--wire-color-[a-z0-9-]+)\s*:/g)].map((match) => match[1]!),
|
||||
);
|
||||
|
||||
const missing = new Map<string, string[]>();
|
||||
for (const name of uiComponentNames()) {
|
||||
const source = readFileSync(uiComponentPath(name), "utf8");
|
||||
for (const match of source.matchAll(/var\((--wire-color-[a-z0-9-]+)/g)) {
|
||||
const token = match[1]!;
|
||||
if (defined.has(token)) continue;
|
||||
if (!missing.has(token)) missing.set(token, []);
|
||||
const users = missing.get(token)!;
|
||||
if (users.length < 4 && !users.includes(name)) users.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
expect([...missing].map(([token, users]) => `${token} <- ${users.join(", ")}`)).toEqual([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user