Files
WRNexusJS/packages/ui/components/Timeline.wrn
T
ClintchizandClaude Opus 5.5 1bed8e4b74 feat(ui): component redesign pass and fixes, release ui 0.8.87
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>
2026-09-30 16:18:13 +05:30

304 lines
14 KiB
Plaintext

component Timeline {
outputs {
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Timeline"
description: string = ""
items: unknown[] = []
variant: string = "default"
numbered: boolean = false
class: string = ""
}
functions {
// Explicit status wins; otherwise the item flagged current is "current"
// and everything after it is "upcoming".
shared function statusOf(item, index, currentIndex) {
if (item.status) { return item.status }
if (item.current) { return "current" }
currentIndex = -1
for (var position = 0; position < items.length; position++) {
if (currentIndex < 0 && items[position] && items[position].current) { currentIndex = position }
}
if (currentIndex >= 0 && index > currentIndex) { return "upcoming" }
return "default"
}
}
view {
<section
data-ui-component="Timeline"
aria-label='{title}'
class='wrn-timeline {class}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
>
{#if title || description}
<header class="wrn-timeline__header">
{#if title}
<h3 class="wrn-timeline__title">{title}</h3>
{/if}
{#if description}
<p class="wrn-timeline__description">{description}</p>
{/if}
</header>
{/if}
<ol class="wrn-timeline__list">
{#each items as item, index}
<li
class="wrn-timeline__item"
data-status='{statusOf(item, index)}'
data-interactive='{item.href ? "true" : "false"}'
@click='output.select({ item: item, index: index })'
>
<span
class="wrn-timeline__marker"
data-status='{statusOf(item, index)}'
>
{#if numbered}
<span class="wrn-timeline__number" aria-hidden="true">{index + 1}</span>
{:else}
<span class='{item.icon || "icon-[lucide--circle-check]"}' aria-hidden="true"></span>
{/if}
</span>
<article
class="wrn-timeline__card"
data-variant='{variant}'
>
<div class="wrn-timeline__card-header">
<div>
<!--
Items sit under this component's own title when it has one.
Without that title they are the first heading here, so they
take its level instead of skipping one and leaving a gap in
the page's heading order.
-->
{#if title}
<h4 class="wrn-timeline__item-title">{item.title || item.label}</h4>
{:else}
<h3 class="wrn-timeline__item-title">{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wrn-timeline__subtitle">{item.subtitle}</p>
{/if}
</div>
{#if item.date || item.time || item.meta}
<time class="wrn-timeline__time">{item.date || item.time || item.meta}</time>
{/if}
</div>
{#if item.description}
<p class="wrn-timeline__copy">{item.description}</p>
{/if}
{#if item.href}
<a href='{item.href}' class="wrn-timeline__action">
<span>{item.actionLabel || "View details"}</span>
<span class="icon-[lucide--arrow-right] wrn-timeline__action-icon" aria-hidden="true"></span>
</a>
{/if}
</article>
</li>
{:empty}
<li class="wrn-timeline__empty">No timeline items available.</li>
{/each}
</ol>
<slot></slot>
</section>
}
style {
.wrn-timeline { --wrn-timeline-accent: var(--wrn-color-primary); width: 100%; min-width: 0; color: var(--wrn-color-text); }
.wrn-timeline[data-color="success"] { --wrn-timeline-accent: var(--wrn-color-success); }
.wrn-timeline[data-color="info"] { --wrn-timeline-accent: var(--wrn-color-info); }
.wrn-timeline__header { margin-bottom: 1.5rem; }
.wrn-timeline__title {
color: var(--wrn-color-text);
font-size: 1.25rem;
font-weight: 650;
letter-spacing: -0.02em;
}
.wrn-timeline__description { max-width: 42rem; margin-top: 0.5rem; color: var(--wrn-color-text-muted); line-height: 1.75rem; }
.wrn-timeline__list { position: relative; display: grid; gap: 1rem; margin: 0; padding: 0 0 0 3rem; list-style: none; }
.wrn-timeline__list::before {
position: absolute;
inset-block: 1rem;
inset-inline-start: 0.9375rem;
width: 2px;
border-radius: 999px;
background: var(--wrn-color-border);
content: "";
}
.wrn-timeline__item { position: relative; min-width: 0; }
.wrn-timeline__item:last-child { padding-bottom: 0; }
.wrn-timeline__marker {
position: absolute;
z-index: 1;
top: 1.125rem;
inset-inline-start: -3rem;
display: flex;
width: 2rem;
height: 2rem;
align-items: center;
justify-content: center;
border: 3px solid var(--wrn-color-bg);
border-radius: 50%;
color: var(--wrn-color-on-primary);
background: var(--wrn-timeline-accent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--wrn-timeline-accent) 40%, var(--wrn-color-border)), var(--wrn-highlight-edge, none);
}
.wrn-timeline__marker:is([data-status="complete"], [data-status="success"]) { background: var(--wrn-color-success); }
.wrn-timeline__marker:is([data-status="warning"], [data-status="pending"]) { background: var(--wrn-color-warning); }
.wrn-timeline__marker:is([data-status="danger"], [data-status="failed"]) { background: var(--wrn-color-danger); }
.wrn-timeline__number {
font-family: var(--wrn-font-mono, monospace);
font-size: 0.72rem;
font-weight: 650;
}
.wrn-timeline__card {
min-width: 0;
padding: 1.125rem 1.25rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius);
background: var(--wrn-color-surface-raised);
box-shadow: var(--wrn-shadow-1);
transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
}
.wrn-timeline__item[data-interactive="true"] { cursor: pointer; }
.wrn-timeline__item[data-interactive="true"]:hover .wrn-timeline__card {
border-color: var(--wrn-color-border-strong, var(--wrn-color-border));
transform: translateY(-1px);
box-shadow: var(--wrn-shadow-2);
}
.wrn-timeline__card[data-variant="minimal"] { padding: 0; border-color: transparent; background: transparent; box-shadow: none; }
.wrn-timeline__card-header { display: flex; flex-direction: column; gap: 0.5rem; }
.wrn-timeline__item-title {
color: var(--wrn-color-text);
font-weight: 620;
letter-spacing: -0.01em;
}
.wrn-timeline__subtitle {
margin-top: 0.125rem;
color: var(--wrn-color-primary-text, var(--wrn-color-primary));
font-size: 0.8125rem;
font-weight: 550;
}
.wrn-timeline__time {
flex: none;
color: var(--wrn-color-text-subtle, var(--wrn-color-text-muted));
font-family: var(--wrn-font-mono, monospace);
font-size: 0.72rem;
font-variant-numeric: tabular-nums;
}
.wrn-timeline__copy { margin-top: 0.75rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
.wrn-timeline__action {
display: inline-flex;
align-items: center;
gap: 0.375rem;
margin-top: 0.875rem;
color: var(--wrn-color-primary-text, var(--wrn-color-primary));
font-size: 0.8125rem;
font-weight: 600;
}
.wrn-timeline__action:hover { color: var(--wrn-color-primary-hover); }
.wrn-timeline__action-icon { width: 1rem; height: 1rem; }
.wrn-timeline__empty { padding-left: 2rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; }
@media (min-width: 40rem) {
.wrn-timeline__card-header { flex-direction: row; align-items: flex-start; justify-content: space-between; }
}
@media (max-width: 39.99rem) {
.wrn-timeline__list { padding-inline-start: 2.5rem; }
.wrn-timeline__list::before { inset-inline-start: 0.75rem; }
.wrn-timeline__marker { inset-inline-start: -2.5rem; }
.wrn-timeline__card { padding: 1rem; }
}
/* --- Status: current rings and pulses, upcoming goes hollow ------------ */
.wrn-timeline__marker[data-status="current"] {
box-shadow: 0 0 0 1px var(--wrn-timeline-accent), 0 0 0 5px color-mix(in srgb, var(--wrn-timeline-accent) 20%, transparent);
animation: wrn-timeline-pulse 2.2s ease-in-out infinite;
}
.wrn-timeline__marker[data-status="upcoming"] {
color: var(--wrn-color-text-subtle, var(--wrn-color-text-muted));
background: var(--wrn-color-surface);
box-shadow: inset 0 0 0 1.5px var(--wrn-color-border-strong, var(--wrn-color-border));
}
.wrn-timeline__item[data-status="current"] .wrn-timeline__card {
border-color: color-mix(in srgb, var(--wrn-timeline-accent) 45%, var(--wrn-color-border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-timeline-accent) 10%, transparent), var(--wrn-shadow-1);
}
.wrn-timeline__item[data-status="upcoming"] .wrn-timeline__card { opacity: 0.72; }
.wrn-timeline__item[data-status="upcoming"] .wrn-timeline__item-title { color: var(--wrn-color-text-muted); }
@keyframes wrn-timeline-pulse {
50% { box-shadow: 0 0 0 1px var(--wrn-timeline-accent), 0 0 0 8px color-mix(in srgb, var(--wrn-timeline-accent) 6%, transparent); }
}
/* --- Card variants ----------------------------------------------------- */
.wrn-timeline[data-variant="soft"] .wrn-timeline__card {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-timeline-accent) 5%, var(--wrn-color-surface-soft, var(--wrn-color-surface)));
box-shadow: none;
}
.wrn-timeline[data-variant="raised"] .wrn-timeline__card {
border-color: var(--wrn-color-border);
box-shadow: var(--wrn-highlight-edge, none), var(--wrn-shadow-2);
}
.wrn-timeline[data-variant="outline"] .wrn-timeline__card {
background: transparent;
border-style: dashed;
box-shadow: none;
}
/* Compact: a dense dot rail with the time inline, for drawers and panels. */
.wrn-timeline[data-variant="compact"] .wrn-timeline__list { gap: 0; padding-inline-start: 1.75rem; }
.wrn-timeline[data-variant="compact"] .wrn-timeline__list::before { inset-block: 0.5rem; inset-inline-start: 0.4375rem; width: 1px; }
.wrn-timeline[data-variant="compact"] .wrn-timeline__marker {
top: 0.625rem; inset-inline-start: -1.75rem; width: 0.875rem; height: 0.875rem; border-width: 2px;
box-shadow: 0 0 0 1px color-mix(in srgb, var(--wrn-timeline-accent) 40%, var(--wrn-color-border));
}
.wrn-timeline[data-variant="compact"] .wrn-timeline__marker > * { display: none; }
.wrn-timeline[data-variant="compact"] .wrn-timeline__card {
padding: 0.4rem 0 0.9rem; border: 0; border-radius: 0; background: transparent; box-shadow: none;
}
.wrn-timeline[data-variant="compact"] .wrn-timeline__card-header { flex-direction: row; align-items: baseline; justify-content: space-between; }
.wrn-timeline[data-variant="compact"] .wrn-timeline__item-title { font-size: 0.875rem; font-weight: 550; }
.wrn-timeline[data-variant="compact"] .wrn-timeline__copy { margin-top: 0.125rem; font-size: 0.8125rem; }
/* Alternate: a centred spine with cards alternating sides once the
timeline itself is wide enough (container query, not viewport). */
.wrn-timeline[data-variant="alternate"] { container: wrn-timeline / inline-size; }
@container wrn-timeline (min-width: 40rem) {
.wrn-timeline[data-variant="alternate"] .wrn-timeline__list { padding-inline-start: 0; gap: 1.5rem; }
.wrn-timeline[data-variant="alternate"] .wrn-timeline__list::before { inset-inline-start: calc(50% - 1px); }
.wrn-timeline[data-variant="alternate"] .wrn-timeline__item { width: calc(50% - 2rem); }
.wrn-timeline[data-variant="alternate"] .wrn-timeline__item:nth-of-type(even) { margin-inline-start: auto; }
.wrn-timeline[data-variant="alternate"] .wrn-timeline__item:nth-of-type(odd) .wrn-timeline__marker { inset-inline-start: auto; inset-inline-end: -3rem; }
.wrn-timeline[data-variant="alternate"] .wrn-timeline__item:nth-of-type(even) .wrn-timeline__marker { inset-inline-start: -3rem; }
.wrn-timeline[data-variant="alternate"] .wrn-timeline__item:nth-of-type(odd) .wrn-timeline__card { text-align: end; }
.wrn-timeline[data-variant="alternate"] .wrn-timeline__item:nth-of-type(odd) .wrn-timeline__card-header { flex-direction: row-reverse; }
}
/* --- Sizes --------------------------------------------------------------- */
.wrn-timeline[data-size="sm"] .wrn-timeline__title { font-size: 1rem; }
.wrn-timeline[data-size="sm"] .wrn-timeline__card { padding: 0.75rem 0.875rem; }
.wrn-timeline[data-size="sm"] .wrn-timeline__item-title { font-size: 0.875rem; }
.wrn-timeline[data-size="sm"] .wrn-timeline__list { gap: 0.625rem; }
.wrn-timeline[data-size="lg"] .wrn-timeline__title { font-size: 1.5rem; }
.wrn-timeline[data-size="lg"] .wrn-timeline__card { padding: 1.375rem 1.5rem; }
.wrn-timeline[data-size="lg"] .wrn-timeline__item-title { font-size: 1.0625rem; }
.wrn-timeline[data-size="lg"] .wrn-timeline__list { gap: 1.25rem; }
@media (prefers-reduced-motion: reduce) {
.wrn-timeline__marker[data-status="current"] { animation: none; }
}
}
}