Skip to content
MetaLaunch UI

Changelog

1.8.0the sidebar: width, first paint, tooltips

Additive. /app/utils: APP_SIDEBAR_WIDTH ({ collapsed: 56, expanded: 208 }), SIDEBAR_ATTRIBUTE, sidebarInitScript(key, default = "collapsed"), Z_INDEX (also from /app; tokens --z-sticky … --z-tooltip-overlay in app/styles/tokens.css, category layer in APP_TOKENS). AppShell: expanded width 208 px (was 240); the width comes from <html data-sidebar> in CSS and the state starts from it (no collapse-then-open on a remount); the width transition runs only after a toggle; chrome prop (default true) for a shell mounted once in a root layout; the main landmark is <div id="main" role="main"> (was <main>; FocusRescue finds either). The rail's tooltips are the kit Tooltip (portalled, side="right"), no longer CSS spans under the page. Tooltip: z-index from --z-tooltip (75: above page content, sticky headers, toolbars and the sidebar; dialogs, popovers, sheets and toasts above it), data-layer="overlay" (--z-tooltip-overlay, 110) when its control is inside a dialog, menu, listbox or popover. Tests in test/app/w2-shell.test.tsx.

1.7.0Screens

Additive, no export added or removed.

  • Five Screens (kind: "screen", category "Screens"), full-page examples built only from kit exports with fictional data (@example.com), in src/screens/: website (section: "blocks") Landing page (Navbar, Hero, a features Grid of Cards, StatsSection, a call-to-action Band, Footer) and Pricing page (Monthly | Yearly SegmentedControl, three PricingCards, an Accordion, Footer); product (section: "product") Application hub (StickyToolbar with ListSearch, QuickFilters, SortMenu and List | Board; ListRows or the Kanban; no Board on phones), Students list (toolbar with a Stage FilterDropdown, rows with PersonName, Badge and Tick, the SelectionBar) and Sign-in (a card with the email field and two Buttons).
  • Each has a catalogue entry with its description, phone and tablet notes, components and source (the screen file); CATALOGUE_SECTIONS adds "Screens" to Website blocks and Product UI.
  • Tests: test/screens.test.tsx (registered with their sections, catalogue fields, imports only kit code, React and lucide-react, only @example.com addresses, every Screen renders); test/catalogue.test.ts accepts kind: "screen" only for category "Screens".

1.6.0W2: the one app sidebar and shell

Additive. /app: AppShell, PhoneNav, SidebarAction, sidebarRowClass, useAppSidebar and the types AppNavItem, AppNavProgress, AppShellBrand, AppShellProps (README "App shell and the one sidebar"); ThemeProvider, useTheme, useColorScheme, DEFAULT_THEME_STORAGE_KEY and NavigationProgress, moved from candidate-portal; Menu bareTrigger. /app/utils: themeInitScript. The base Sidebar measures its pill through the shared lib/traveling-pill.ts (no change in look or API). Docs: the app-sidebar product demo (its CATALOGUE entry: Product UI, "Shell & navigation", with responsive and usedIn). Codemod codemods/waves/w2.json. Tests test/app/w2-shell.test.tsx.

1.5.0the catalogue

Additive, no export removed.

  • CATALOGUE, CATALOGUE_SECTIONS, KIT_APPS, catalogueSource and their types (CatalogueEntry, DemoMeta, DemoKind, DemoSection, DemoStatus, DemoResponsive, KitApp) from the root entry and /internal-demo-registry. Every entry, base ones too, has a section, category (base categories copied once from docs-dev's old map), description, status, responsive note and usedIn. New section "blocks" (Website blocks): hero, overlap-hero, feature-showcase, segmented-sections, modal-cards, step-walkthrough, scroll-screens, device-frame, loop-video, pricing-card, stats-section, band, section-divider, sticky-mobile-cta, footer. meta.section may now be "blocks" (readers that only know "product" treat it as base, as before).
  • meta.kind ("component" by default, or "screen"; no screens yet).
  • Product demo modules keep only layout / previewHeight in their own meta; their section, category, import path and components come from the catalogue through demoRegistry (unchanged for readers).
  • APP_TOKENS, THEMES, SCREEN_SIZES, TAILWIND_SCREENS. BREAKPOINTS is unchanged (moved to lib/screens.ts, still exported from the root).
  • Both Tailwind presets set theme.screens from BREAKPOINTS (identical to Tailwind's defaults).
  • docs-dev reads the catalogue: sections Website components, Website blocks, Product UI with categories in catalogue order; its copied CATEGORIES and PRODUCT maps are gone; the brand picker reads THEMES.
  • Tests: test/catalogue.test.ts.

1.4.0Product UI in the docs

Additive. The demo registry (@metalaunchlabs/ui/internal-demo-registry) now covers the components moved in from candidate-portal (K1, W1), as a separate Product UI section:

  • 31 demo modules in src/product-demos/ (fictional data, @example.com people), each rendering inside frame.tsx: UiHostProvider with links and navigation kept on the page, in-memory storage and a demo viewer, and CandidateScope, so nothing needs Next. /app: app-button (Button, ButtonLink), icon-button, app-badge, tag-chip (TagChip, TagChips, TagDot), person-name, app-empty-state, app-tooltip, app-scroll-fade, disabled-reason (ReasonTip, ReasonText, ReadOnlyReason), accessibility (FieldError, LiveStatus), app-tabs (Tabs, TabPanel, SectionSwitcher), segmented, kanban, list-row (ListRow, ListRows, RowCount), list-more (ListMore, RowSkeletons), list-toolbar (StickyToolbar, ListSearch, QuickFilters, SortMenu), filter-dropdown, filter-panel (FilterPanel, ActiveFilters), selection-bar (SelectionBar, Tick, SelectHit), menu, app-dialog (Dialog, ConfirmDialog), bottom-sheet, app-popover (PopoverPanel), toaster, save-bar (SaveBar, UnsavedChangesGuard); /resources: status-badge (StatusBadge, StatusPicker), application-labels (ChancesBadge, ChancesPicker, TopChoiceStar, TagChips), intake-select; /platform: level-badge, email-chips (EmailChips, ChipFieldFrame), people-picker. Product slugs that would clash with a base slug take an app- prefix.
  • A module's meta may now carry, all optional so the existing entries are unchanged: section ("base" by default, or "product"), importPath (default @metalaunchlabs/ui, e.g. @metalaunchlabs/ui/app), category (set on every product entry: Primitives, Navigation, Lists & boards, Overlays, Feedback & saving, Forms & pickers, Resources, Platform) and components (the exports the page covers). New exports from the registry entry: the DemoMeta type, DEFAULT_DEMO_SECTION, DEFAULT_DEMO_IMPORT_PATH.
  • test/demo-coverage.test.tsx: every public component export of every entry has a demo (or a stated reason: ThemeScope, InputModality, UiHostProvider, CandidateScope, FocusRescue, SaveHeld), product entries name real exports in their importPath, and every product demo renders inside the candidate scope.
  • docs-dev: the sidebar and index show Components and Product UI sections; the crawler reads src/product-demos/ with a category and props source per slug.

The product demos look right only where the host loads the app layer's CSS compiled with the app Tailwind preset (README "Layers"); a precompiled, scoped stylesheet for docs and embedding hosts is still to come.

1.3.0W1: primitives, list pieces, badges, sharing pieces

Additive. Moved from candidate-portal verbatim, its couplings replaced by the host seams (links through useHostLink(), the guard's navigation through useHostRouter()), module-level state through singleton():

  • /app primitives: Badge (BadgeTone), EmptyState, PersonName with personDisplay, personText (PersonKind, PersonInput; names with email), Segmented, Tabs (TabPanel, tabPanelProps; phones get the SectionSwitcher or phone="wrap"), SectionSwitcher, ListRow / ListRows / RowCount, PAGE_CONTAINER, Menu (anyMenuOpen, openCardMenuAt; a bottom sheet on phones), BottomSheet, Dialog / ConfirmDialog (a sheet on phones, full screen option), FocusRescue (focusTrail, noteFocus, rescueFocus), usePopover / PopoverPanel (a sheet on phones), the anchored-list helpers, IconButton, chipClass, TagChip / TagChips / TagDot with tagTone, normaliseTag, nextRadioIndex; isTypingTarget (also in /app/utils).
  • /app feedback: Toaster (toasts with Undo), SaveBar (focusFirstInvalid), UnsavedChangesGuard.
  • /app lists: FilterDropdown (name plus count), QuickFilters, SortMenu (icon), StickyToolbar, ListSearch, SelectionBar, usePagedList (ListPage, ListCounts, FetchPage); new, generic: FilterPanel (the Filter icon button and panel: search, toggles, groups with counts, Clear) and ActiveFilters (removable chips), with filterCount, FilterGroup, FilterGroupOption, FilterToggle.
  • /resources: the Application labels (APPLICATION_STATUSES, statusLabel, statusTone, APPLICATION_CHANCES, chancesLabel, chancesTone, isOutcome, INTAKE_KEY, intakeKeyLabel, sortIntakeKeys), StatusBadge, StatusPicker, ChancesBadge, ChancesPicker, TopChoiceStar, IntakeSelect.
  • /platform (sharing, new): LevelBadge (SHARE_LEVEL_LABELS, ShareLevel), ChipFieldFrame with the Add people chip classes, EmailChips, and PeoplePicker (type-to-search people as chips with Lead stars; normLeads, PickerPerson, PeopleWithLeads).

Codemod for candidate-portal: codemods/waves/w1.json (it also retires the W0 shims lib/toast and lib/staged).

1.2.0K1: Kanban

Additive. /app gains Kanban (with KanbanLane, KanbanCardContext, KanbanProps, kanbanCountText, DRAG_THRESHOLD_PX, EDGE_SCROLL_PX), taken from candidate-portal's ApplicationBoard (the Application card stays in candidate-portal as a render prop), and the primitives it needs, moved verbatim from candidate-portal ahead of W1: Button, ButtonLink (links through useHostLink()), buttonClasses, phoneLabelClasses, useIsPhone, Tooltip (its one-at-a-time slot is a singleton()), REASON_TOOLTIP_DELAY_MS, HOVER_GRACE_MS, LONG_PRESS_MS, the disabled-with-a-reason helpers, ScrollFade and its helpers, ListMore, RowSkeletons, useInfiniteScroll (LoadMoreState), Tick, SelectHit, useAnnouncer, LiveStatus, FieldError, describedBy, withContext, and cn (also in /app/utils). Codemod for candidate-portal: codemods/waves/k1.json.

1.1.2W0: CSS sync

Patch; no API change. app/styles/components.css takes candidate-portal's components-layer rules added since 1.1.1 (the 44 px touch hit areas for buttons and 16 px text fields on touch phones, the bottom sheet's 44 px rows, and the Board's always-visible .board-scrollbar), copied by scripts/sync-candidate-css.mjs --write, so W0's switch imports exactly today's rules.

1.1.1W0: CSS sync

Patch; no API change. app/styles/components.css takes candidate-portal's muted-control hover rules (candidate-portal 2119fee, sharing D26: .btn, .btn-ghost, .btn-danger, .btn-outline, .btn-secondary, .btn-destructive and .icon-btn with aria-disabled='true' keep their resting look on hover), copied by scripts/sync-candidate-css.mjs --write, so candidate-portal's W0 switch imports exactly the rules its globals.css has today.

1.1.0W0: the layers' foundations

Additive; the core entry is unchanged (ADR-0021, platform-hq efforts/candidate-ui-to-kit W0).

  • New entries: @metalaunchlabs/ui/app, /app/utils, /app/tailwind-preset, /app/styles.css, /app/styles/*.css, /platform, /candidate, /resources (README "Layers"). The layers build in a second tsup config into dist/layers/ with a "use client" banner; /app/utils and the preset without it.
  • The candidate look: candidate-portal's Tailwind theme as the app preset, and its tokens, base rules and component classes (globals.css lines 22-626, verbatim) as app/styles/*.css, scoped under [data-ui="candidate"] by scripts/build-layer-css.mjs with zero added specificity.
  • Host seams: UiHostProvider (link, router, pathname, search params, storage, viewer, formatError; all optional, nested providers merge) with its hooks, and configureUiRuntime.
  • Runtimes moved from candidate-portal, behaviour unchanged and single-copy (globalThis singletons): toast / useToasts / toastDuration, useStaged / useStagedForm / useStagedRegistration / useAnyDirty / SaveHeld / navigateGuarded / useNavRequest.
  • CandidateScope, candidateScopeProps, UI_SCOPE_ATTRIBUTE, CANDIDATE_SCOPE.
  • New dependency: zustand ^4.5.5 (layers only).
  • Tests: test/app-runtime.test.tsx, test/app-styles.test.ts.
  • Tooling (repo only, not in the bundle): scripts/sync-candidate-css.mjs (drift between the app layer's CSS sources and candidate-portal's globals.css until it imports them) and codemods/waves/w0-globals.mjs (candidate-portal's globals.css switch).

1.0.0the semver promise

No API change from 0.9.1: the same exports, entry points, props and styles. What 1.0.0 adds is a promise (ADR-0022):

  • Minors are additive and patches are fixes; breaking changes happen only in a major, and only for something deprecated (with a dev-time warning, a DEPRECATIONS.md row and, where mechanical, a codemod) for at least two minors before.
  • Old versions stay published; every app upgrades on its own schedule with pnpm kit:upgrade, and can roll back the same way.
  • Every release runs every consumer's typecheck, tests and build first (npm run consumers:check).

Release tooling (not in the bundle): the public API snapshot (api/public-api.json, test/public-api.test.ts, npm run api:update / api:check), the internal deprecated() helper, DEPRECATIONS.md, codemods/ (shipped in the package), scripts/consumers-check.mjs and scripts/kit-upgrade.mjs (repo root).

0.9.1

  • FeatureShowcase: running text (the description and each item's answer) is 16 px on phones and 14 px from sm up, so body copy is never under 16 px on a phone (mobile audit 2026-10-25; the site's design-qa phone check).

0.9.0mobile-native building blocks

New components (container- or touch-native, hydration-safe, axe clean):

  • SnapCarousel: a swipeable row on CSS scroll-snap (native momentum, no JS drag), the next card peeking (slideWidth, default 85%), dots up to 7 slides then a "2 / 5" counter (indicator), previous / next buttons with a fine pointer (controls="auto"), arrow keys and Home / End, aria-roledescription="carousel" with slides named "2 of 5", polite announcements, the ScrollFade edge fade, bleed to run to the screen edges on the page gutter. 44 px targets on touch. nearestSlide() exported. (Carousel stays for auto-advancing promo panels.)
  • StepWalkthrough: an annotated screenshot shown one step at a time, zoomed into each region (source px) by a CSS transform (zoomTransform()), caption below, Back / Next, swipe and arrow keys, "Step 2 of 4" announced. Sharp: the default sizes asks for the viewport times the deepest zoom. From 640 px of its own width (container query, breakpoint) it shows the whole image with numbered markers and the steps listed. Reduced motion: no glide.
  • StickyMobileCTA: a bottom bar for one or two actions on phones only (md:hidden), shown once trigger (the hero) has scrolled above, hidden while hideWhen elements (default the footer; add the in-page CTA) are visible and while a text field has focus, padded by --safe-bottom; reserves scroll-padding-bottom while shown (WCAG 2.4.11).
  • SegmentedSections: columns when its box is wide, a segmented control with one panel when narrow (pricing, comparisons); container query; tab semantics when narrow.
  • LoopVideo: a muted looping product clip. The server renders only the poster <img>; a preload="none" video mounts after hydration and gets its sources only within rootMargin (200px) of the viewport; plays while visible, pauses off screen and in a hidden tab; never loads under reduced motion; a Pause / Play button always visible (WCAG 2.2.2).

Extended:

  • Sheet side="bottom" is a native bottom sheet: rounded top, a grab handle you drag down to close (dragToClose, default true; SHEET_DRAG_CLOSE_PX), up to 85dvh, padded above the home indicator.
  • SegmentedControl: role="tablist" (with idPrefix; segmentedTabIds()) to switch panels, and size="lg" (44 px). Default stays a radio group.
  • Hooks: useMediaQuery, useBreakpoint, useIsMobile, BREAKPOINTS, minWidthQuery (lib/breakpoints.ts), for behaviour, not layout.
  • Tokens (new category spacing): --gutter-mobile (20 px), --safe-top, --safe-right, --safe-bottom, --safe-left; preset spacing gutter-mobile, safe-* (px-gutter-mobile, pb-safe-bottom); classes .mll-gutter, .mll-safe-top, .mll-safe-bottom.

Fixes from the marketing site's mobile audit (platform-hq efforts/marketing-website/research/mobile-audit-2026-10-25.md):

  • FeatureShowcase: side gutters at every width. scheme="auto" had none, so its text ran to 0 px from the screen edges on phones; it now pads by --gutter-mobile (or the safe area), 32 px from md. The dark scheme keeps 24 / 48 px and respects the safe area.
  • StatsSection (and so FeatureShowcase stats): a stat already on screen at load shows its final number (no count from 0 mid-read); others count as soon as they enter, in 700 ms (was 1.2 s from 10% inside). Reduced motion: final numbers, as before.
  • Band gutter (opt-in, off by default so existing layouts are unchanged): pads content by the phone gutter when it has no container of its own.

Tests: test/mobile-primitives.test.tsx, test/loop-video.test.tsx, test/audit-fixes.test.tsx; the hydration suite renders the new components. Checked in Chromium at 320, 390 and 1280 px, light and dark (axe clean).

0.8.0

  • Band tones tint and tint-secondary: the light-mode versions of the emphasis tones. In light mode they are a soft primary (or secondary) tint, so a light page never breaks into a dark block; in dark mode they are the inverse (or secondary) surface. Content keeps the page's tokens; no pattern on them (one effect per section). Tokens --surface-tint and --surface-tint-secondary in every theme, both modes; aliases --band-tint and --band-tint-secondary.
  • FeatureShowcase scheme="auto": follows the page's mode on a transparent background, without its own dividers, for use inside a Band (default "dark" is unchanged).
  • ButtonIcon: the signature arrow circle on its own, for links styled as buttons (<Link className={buttonVariants()}><span>Label</span><ButtonIcon /></Link>); Button uses it.
  • Tests: test/tint-and-button-icon.test.tsx.

0.7.3

  • StatsSection: columns are chosen from the number of stats so every row is full (no stat left alone on a row): up to 3 sit in one row at every width (smaller type on phones), 4 are 2×2 on phones and one row from md, larger counts use the widest even split (statsColumns(), data-stats-cols). FeatureShowcase no longer overrides the grid. Tests: test/stats-columns.test.tsx.

0.7.2

  • FeatureShowcase: the frozen belt's text is full-strength primary (was 90%), so it passes 4.5:1 on the lighter dark-mode inverse surface from 0.7.1 (was 4.28:1).

0.7.1

  • FeatureShowcase:
  • The frozen tilted belt is 140vw and centred, so its ends always fall outside the section (no corners on screen), with clear space above the stats.
  • Stats spread evenly across the content width.
  • Labels (the top eyebrow and item eyebrows) are sentence case: no ALL-CAPS or letter-spacing.
  • An item without a description renders as a plain row, not a trigger with a dangling aria-controls.
  • axe is clean in light and dark.
  • Dark surfaces, measured in CIE L* (page 17.8, muted 22.3):
  • --surface-inverse dark is L* 31.5 (231 8% 30.6%, was 26.8), +9.2 from muted and +13.7 from the page, so curves between them read.
  • Other brands' dark inverse is muted +9 L*.
  • --surface-primary dark is 78 32% 61.5% (L* 73.6, less chroma than 0.7.0's L* 74, which glared): ink 6.83:1, ink at 80% 4.53:1. A lime below about L* 73 cannot keep 80% ink at 4.5:1, so the glare is reduced through saturation rather than lightness.
  • Teal is unchanged (white 7.81:1).
  • Tests: test/showcase-and-tones.test.tsx (no caps classes, belt width, even stats, plain rows, dark-tone L* targets for every brand). vitest testTimeout raised to 20s: the hydration suite's cold start exceeds 5s on a loaded machine.

0.7.0

  • Band edges: curve: "convex" | "concave" from the band's own view, correct on both sides (a concave top used to paint the neighbour as a lens on the wrong side, with a strip and a hard edge). edgeGeometry() is exported; BandEdge.flip is deprecated and keeps its old behaviour. Verified in a browser for every side × curve on four tone pairs, light and dark: only the two band colours at each seam.
  • SectionDivider: fill: "below" | "above" (the complement of the shape).
  • Curves are shallower (32 px on phones to 80 px wide) and the padding next to an edge is its depth plus the normal padding.
  • Band lift: content rises 24 px to rest on entry (not on bands with images).
  • Hydration-safe motion: new useMotionAllowed() (false on the server and first client render). StatsSection, TextReveal, ScrollReveal, ScrollScreens, Cursor, ModalCards, Hero and Band render the same markup on the server and the client whatever the reduced-motion setting (fixes React errors 418/423/425 and the root re-render that dropped html.dark); Band edges render at full depth on the server and never move under reduced motion. Test: test/hydration.test.tsx.
  • Surfaces by CIE L* (metalaunchlabs; light / dark): page 96.1 / 17.8; muted 94.1 / 22.3; inverse 17.8 / 26.8. New --surface-primary and --surface-secondary (light = the accent; dark = softened): lime dark 78 51% 52.1% (6.3 L* softer), ink text 6.93:1 at 100% and 4.60:1 at 80%; teal dark 181 23% 28.1%, white text 7.81:1 (light: lime 8.33:1 / 5.22:1 at 80%, teal 5.03:1). Preview brands: mono-slate and preview-blue primaries and sunset-coral and midnight-violet secondaries were already under 4.5:1 in light mode; unchanged here.

0.6.0

  • Band: one effect per section. A pattern renders only on calm tones (background, muted, card); on inverse, primary and secondary it is ignored, with a development warning. Docs and demos follow the rule.

0.5.1

  • Band: muted no longer adds grain automatically. Edges are flat fills, so grain beside a curved edge showed a faint step; textures are now opt-in (pattern).

0.5.0

  • Band: a full-width section in one tone (background, muted, card, inverse, primary, secondary), optionally textured (grain, dots, grid, stripes, ruled; static), with SectionDivider edges on either or both sides painted in the neighbour's tone. Edges deepen from nearly flat as they scroll into view; static under reduced motion. Fills are frozen on <html> (--band-* in defaults.css) so an edge inside an inverse band paints the page's colour. Docs: band.mdx; tests: test/band.test.tsx.
  • Tokens --surface-muted and --surface-inverse (schema keys surfaceMuted, surfaceInverse) in every brand, light and dark.
  • BrowserFrame (address bar) and PhoneFrame for product screenshots.

0.4.1

  • Table: the actions column's sr-only "Actions" label is contained by its header cell, so it no longer escapes the horizontal scroller and widens the page on a phone.

0.4.0accessibility, WCAG 2.2 AA; audit in platform-hq `efforts/accessibility`

  • Contrast. Default and metalaunchlabs status tokens now pass 4.5:1 as text and with their foreground as a fill: light mode uses deep shades with white foregrounds (warning's foreground is now white), dark mode light shades with ink foregrounds. --secondary is 181 23% 38% (was 43%). Muted text in components is at least /70 (was /40–/60), placeholders /70, decorative icons /60; Badge primary and Avatar initials use ink on a tint. The preview brands (preview-blue, mono-slate, midnight-violet, sunset-coral) are unchanged.
  • Keyboard. Dialog and Sheet: ⌘/Ctrl+Enter runs the primary action. New <InputModality /> sets html[data-modality]; with it, base.css hides focus rings on buttons, links, tabs, options and menu items after a mouse action, and overlays always return focus to their trigger (a trigger-less Dialog returns it to what had focus). Render it from a Client Component.
  • Names. Select takes aria-label, aria-labelledby, id, name, required. ProgressBar is named by its label or aria-label. TextInput/Textarea tie errorMessage to the field (aria-describedby). Table: search box named, Rows per page select named, row checkboxes named after the first column, aria-sort, scope="col", named actions column. Breadcrumbs, Pagination, Sidebar take aria-label.
  • Scrolling and targets. ScrollFade is a tab stop while it overflows; ScrollArea is a tab stop and takes label (role region). Carousel and ScrollScreens dots have 24px targets.
  • Reduced motion. Entrance, exit and reveal keyframes finish at once under prefers-reduced-motion; spinners, pings and shimmer keep running.
  • Tests: test/accessibility.test.tsx (axe-core structural rules in jsdom, keyboard, names).

0.3.0

  • ScrollFade + useScrollFade (+ measureScrollFade, scrollFadeMask): a row that scrolls sideways fades out on the side where more content waits, nothing when it fits (fe.md §22). A CSS mask (correct in light and dark), inline styles only (works without the Tailwind preset), updates on scroll / resize / content change, RTL-aware, and keyboard focus scrolls the item clear of the fade (instant under reduced motion).
  • Tabs: the tab list scrolls sideways with the fade when the tabs don't fit (triggers no longer wrap or squash).
  • SegmentedControl: max-w-full; scrolls sideways with the fade when the segments don't fit.
  • Table: the horizontal scroller is a ScrollFade.

0.2.0

New components (app-screen patterns, first used by MetaLaunch, the candidate product):

  • PageHeader — title, one primary action, optional status / info / back slot / ⋯ menu.
  • Nudge (+ nudgeVariants, nudgeActionClassName) — the one attention line per screen; warning (default) or primary tone.
  • SaveStatus — Saving… / Saved / Couldn't save · Retry, in a polite live region.
  • StarRating + StarRatingInput — read-only rating and an accessible five-star radio group.
  • SegmentedControl — side-by-side mutually exclusive choices (radio-group semantics).

Extensions (all backwards compatible):

  • Button: variant="danger" (filled, for destructive confirmations).
  • Badge: forwards HTML attributes (title, id, aria-*, …).
  • Dialog: trigger optional (fully controlled use), title/description accept nodes, eyebrow, hideDescription, role="alertdialog", dismissible, initialFocusRef; long bodies scroll inside the dialog.
  • DropdownMenu: item label accepts nodes (+ textValue, key), icon accepts an element, onOpenChange.
  • Tabs: item content optional (strip-only use), label (list name), listClassName, node labels.
  • EmptyState: title/description accept nodes.
  • InfoTooltip: label → the trigger is named "About {label}".
  • DropdownMenu: loop (arrow keys wrap) and aria-label.
  • ToastProvider: viewportClassName (e.g. lift above a phone bottom nav).
  • Package tests (vitest) added.