Redesign - Shared field foundation rules (duplicated across 15 form components and emitted globally) are now identical everywhere, so later copies no longer silently override earlier redesigns. - Drawn checkbox and radio controls, white switch thumbs (the thumb was the text colour, a black blob in light mode), recessed field wells, keycap actions in Modal, Drawer, Popover, Toaster, Pagination, Stepper, DataTable. - Menus (Dropdown, ContextMenu, Popover, Select dropdown, Navbar panels) share one flat raised panel with neutral hover rows and mono section labels. - Alerts with an accent rail and icon chip; card headers without the grey band; accordion, collapse, list, list group, timeline, stats, metrics, chat, skeleton, avatar group, clipboard, splitter, section, sidebar, navbar and upload progress restyled. - Chart rebuilt: nice y-axis ticks, gridlines, rounded bars, line and area modes, hover tooltips, hidden series leave the scale. Fixes - Checkbox and radio labels toggle their input again (empty for="" broke it). - TimePicker kept every clicked time pressed; only the chosen hour and minute are pressed now, and nothing is pressed before a value exists. - ColorPicker hex readout now follows the picked colour. - Disabled FileInput no longer shows the native file-input text. - Date and time panels float instead of pushing content. - ContextMenu opened at the pointer closes on scroll. - Drawer left/top/bottom placements slide from their own edge again. - Image-only Card no longer renders an empty body strip. - Timeline supports item.time, current/upcoming states, sizes and compact/alternate variants; showcase demos use distinct variants. - ChatBubble: delivery receipts (sending/sent/delivered/read/failed/error) and meta aligned to its bubble. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
5.1 KiB
Plaintext
186 lines
5.1 KiB
Plaintext
// Scrollspy -- a table of contents that follows the reader.
|
|
//
|
|
// <Scrollspy items='[{"label":"Overview","href":"#overview"}]' />
|
|
//
|
|
// 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.
|
|
//
|
|
// The runtime writes the marker straight onto the links rather than into
|
|
// component state. An IntersectionObserver callback fires long after the
|
|
// client function that registered it has returned, and a state write made
|
|
// there is dropped -- so the DOM is the only place the answer can live.
|
|
//
|
|
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
|
// and silently swallows the rule that follows it.
|
|
component Scrollspy {
|
|
outputs {
|
|
change(payload: { href: string; label: string })
|
|
}
|
|
|
|
props {
|
|
color: string = "primary"
|
|
size: string = "default"
|
|
items: unknown[] = []
|
|
active: string = ""
|
|
label: string = "On this page"
|
|
heading: string = ""
|
|
sticky: boolean = false
|
|
topOffset: string = "6rem"
|
|
sectionOffset: string = "7rem"
|
|
collapseAt: string = "800px"
|
|
class: string = ""
|
|
}
|
|
|
|
functions {
|
|
shared function itemList() {
|
|
return Array.isArray(items) ? items : []
|
|
}
|
|
|
|
shared function isActive(item) {
|
|
return Boolean(item.href) && item.href === active
|
|
}
|
|
}
|
|
|
|
view {
|
|
<nav
|
|
{...attrs}
|
|
data-ui-component="Scrollspy"
|
|
class='wrn-scrollspy {class}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-sticky='{sticky ? "true" : "false"}' data-collapse-at='{collapseAt}' style='--wrn-scrollspy-top: {topOffset}; --wrn-scrollspy-section-offset: {sectionOffset};'
|
|
data-wrn-scrollspy="true"
|
|
role="navigation"
|
|
aria-label='{label}'
|
|
>
|
|
<p class="wrn-scrollspy__heading" data-show="heading">{heading}</p>
|
|
|
|
<ul class="wrn-scrollspy__list">
|
|
{#each itemList() as item}
|
|
<li class="wrn-scrollspy__item">
|
|
<a
|
|
class="wrn-scrollspy__link"
|
|
href='{item.href || "#"}'
|
|
data-active='{isActive(item)}'
|
|
aria-current='{isActive(item) ? "location" : "false"}'
|
|
>
|
|
<span class="wrn-scrollspy__label">{item.label}</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<slot />
|
|
</nav>
|
|
}
|
|
|
|
style {
|
|
.wrn-scrollspy {
|
|
--scrollspy-accent: var(--wrn-color-primary);
|
|
max-width: 100%;
|
|
}
|
|
.wrn-scrollspy[data-sticky="true"] { position: sticky; top: var(--wrn-scrollspy-top); height: max-content; }
|
|
.wrn-scrollspy__link { scroll-margin-top: var(--wrn-scrollspy-section-offset); }
|
|
|
|
.wrn-scrollspy:not(:has(.wrn-scrollspy__item)):not(:has(slot > *)) {
|
|
display: none;
|
|
}
|
|
|
|
.wrn-scrollspy[data-color="secondary"] {
|
|
--scrollspy-accent: var(--wrn-color-secondary);
|
|
}
|
|
|
|
.wrn-scrollspy[data-color="success"] {
|
|
--scrollspy-accent: var(--wrn-color-success);
|
|
}
|
|
|
|
.wrn-scrollspy[data-color="danger"] {
|
|
--scrollspy-accent: var(--wrn-color-danger);
|
|
}
|
|
|
|
.wrn-scrollspy[data-color="info"] {
|
|
--scrollspy-accent: var(--wrn-color-info);
|
|
}
|
|
|
|
.wrn-scrollspy[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wrn-scrollspy[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wrn-scrollspy__heading {
|
|
margin: 0 0 0.5rem;
|
|
color: var(--wrn-color-text-subtle, var(--wrn-color-text-muted));
|
|
font-family: var(--wrn-font-mono, monospace);
|
|
font-size: 0.65rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.wrn-scrollspy__list {
|
|
display: grid;
|
|
gap: 0.1rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
border-left: 1px solid var(--wrn-color-border);
|
|
}
|
|
|
|
.wrn-scrollspy__link {
|
|
display: block;
|
|
padding: 0.3rem 0.75rem;
|
|
margin-left: -1px;
|
|
border-left: 2px solid transparent;
|
|
color: var(--wrn-color-text-muted);
|
|
font-size: 0.85rem;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.wrn-scrollspy__link:hover {
|
|
color: var(--wrn-color-text);
|
|
}
|
|
|
|
.wrn-scrollspy__link:focus-visible {
|
|
outline: 2px solid var(--scrollspy-accent);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
.wrn-scrollspy__link[data-active="true"] {
|
|
border-left-color: var(--scrollspy-accent);
|
|
color: var(--wrn-color-text);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/*
|
|
* A table of contents is a sidebar affordance. On a phone it stops being
|
|
* a rail and becomes a horizontal strip that scrolls, so it costs one
|
|
* line rather than a screenful.
|
|
*/
|
|
@media (max-width: 767px) {
|
|
.wrn-scrollspy[data-collapse-at="800px"] { position: static; }
|
|
.wrn-scrollspy__list {
|
|
grid-auto-flow: column;
|
|
grid-auto-columns: max-content;
|
|
overflow-x: auto;
|
|
border-left: 0;
|
|
border-bottom: 1px solid var(--wrn-color-border);
|
|
}
|
|
|
|
.wrn-scrollspy__link {
|
|
margin-left: 0;
|
|
border-left: 0;
|
|
border-bottom: 2px solid transparent;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.wrn-scrollspy__link[data-active="true"] {
|
|
border-left-color: transparent;
|
|
border-bottom-color: var(--scrollspy-accent);
|
|
}
|
|
}
|
|
}
|
|
}
|