Repository navigation
feat: spec-driven component showcase (replaces Storybook) - #6
Conversation
…otion, layers, icons, overview (wip)
- Colors page: stacked swatch bars (ColorStack) replace the token-row tables; values measured live off each bar's computed background, hover tooltip shows token + resolved value, click copies the token - Controls panel: option lists switch to menu vocabulary (OptionRows = menuItemClass rows + check indicator, never a selected row background) - Code panel: Code/Usage switches to library Tabs (pill variant) — panel switching is Tabs' job, not a value picker's - Sidebar/nav rows: fixed 2rem rows with a 2px seam (gap-0.5) so adjacent fills never fuse, mirroring the chat sidebar's row rhythm
- ColorStack ink: composite translucent tokens over the card backdrop before picking dark/light label ink — alpha-blind luminance put white text on near-white overlay bars and dark text on near-black ones - Colors page: single-family sections pair side by side in the page grid instead of stacking full-width one per row - Overview: replace the link list with live preview cards — every Reference component renders its default state in an inert card that links to its page - Controls rows: back to the sidebar row vocabulary (left-aligned, [data-ui-selected] fill, 2px seams); Button's 10-option variant enum drops its forced radio-list in favor of a Select
A one-line collapsed preview carried no information, glued the snippet against the tab bar, and added a state to manage. The snippet is now always fully visible; long ones scroll inside a capped height.
…audit - Section headings move to the contract's 8000 title spec (text-title + font-semibold); sidebar group labels and control group headings adopt the menu-label vocabulary (text-body + medium); control row labels use the field-label spec (text-label + medium) - Top bar removed: the wordmark in the sidebar is home, the selected row is the breadcrumb; prev/next arrows removed as redundant with the nav - Controls panel lives in a right rail that collapses to an icon-width strip with an animated width transition - Code snippets render inside a framed muted-soft block instead of bare text on the page background - Sidebar foot fuses with the list via a gradient fade instead of a hard clip
- Left sidebar collapses to zero width (animated); its close affordance sits in its own header next to the wordmark, its reopen affordance in the tab bar - New content-area tab bar: page title on the left, back/forward and the controls toggle on the right — it spans only the right section, so the sidebar and content read as two independent panels - Back/forward are real browser-style history (stack + pointer), not manifest-order prev/next - Right controls rail collapses to zero width on the same curve; its toggle moves into the tab bar
Both tabs now share one framed container in CodePanel — bare prose on the page background read as unfinished next to the framed code block. The frame moves out of CodeBlock so the tab switch only swaps content.
Chrome icon buttons were default-ghost squares with 2px-stroke icons — off the app's chrome language. One shared ChromeIconButton now carries the topbar recipe (ghost circle at size-7, muted ink warming to foreground on hover) and every shell icon passes the app's 1.75 stroke; back/forward switch to the app's chevron icons.
The sidebar header was a pt-2 + h-8 ad-hoc row (~40px) against the tab bar's h-11 — two titles on two baselines and a bottom seam that stopped at the panel edge. The header now matches h-11 exactly and carries the same bottom hairline, so the seam runs unbroken across both panels and both titles sit on one baseline.
The canvas was a single component floating in a void. Three stage modes now share it: Single (the live instance the controls drive), Examples (every preset tiled with labels, frozen at preset state), and Matrix (two declared axes crossed over defaults — Button ships variant × size). A split toggle renders the stage twice, the right column a scoped .dark subtree, for side-by-side theme review.
Badge gains exported variant/size/font keys (same mirrored-keys pattern as Button) so the spec's controls stay single-source. Dialog is the overlay exemplar: the open state is pinned as a control so the canvas reviews the open surface without a click, with the trigger button kept as the realistic entry point.
AGENTS.md's showcase section now documents the three stage modes and the matrix opt-in, the open-pin rule for overlay specs, and that Colors measures computed backgrounds (useTokenValue is gone — the Colors page was its only consumer).
Second spec batch: display surfaces (badge, card, tabs, accordion, skeleton) and overlays (dialog, popover, dropdown-menu, tooltip, sonner). Overlays follow the dialog open-pin pattern; tooltip's snippet deliberately omits the pin because real usage is uncontrolled. Where a component lacked exported axis keys, the export was added next to its definition (badge/button mirrored-keys pattern; accordion/tabs/ dropdown-menu/tooltip/popover/sonner gained theirs). The segmented spec's stale never-pill guidance is updated for TabsList's pill variant.
- Specs whose render returns an array (dialog, sonner) blanked the single-mode canvas — <component :is> takes a VNode, not an array. The stage body is now normalized through a Fragment. - The examples wall had no clearance for the stage's floating chrome: the first preset's label slid under the mode switcher and read as overlapping layers. pt-14/pb-16 reserves the chrome's space. - The split toggle's sun-moon glyph was one of the busiest icons in the set and read as a dark blob at 16px; it is now the clean half-dark contrast glyph. All floating stage chrome anchors to the left column so nothing sits over the dark comparison column in light theme.
Overlay specs pin open=true so their own page can review the open surface — and the Overview's live preview cards dutifully rendered a real modal dialog (plus floating tooltips/menus) over the landing page. Thumbnails now force any open state closed: the trigger element is the honest preview for an overlay.
Entering the Dialog page fired a real scrim modal on arrival — hostile, and unnecessary: the trigger button plus the two-way open control is the natural flow (open it, then keep tweaking while it stays open). All overlay specs now default open=false; examples pin open in their preset state (showing the open surface is their job), and matrix mode forces open per cell so positioner grids keep working.
…ckfile 0fdaa14 accidentally dropped the menuChromeClass/selectTriggerClass re-export lines from src/index.ts (and rewrote the file with CRLF endings) while removing Storybook wiring; apps/web imports those names, so the app build broke against this branch. The standalone pnpm-lock.yaml also drifted from package.json when the font dev-deps were added, failing frozen-lockfile installs.
gen-entry.ts only scanned components/* and joined lines with CRLF, so every pnpm build silently dropped the hand-maintained lib/menu + lib/trigger named re-exports from src/index.ts (breaking app builds against the package source) and rewrote the whole file's line endings. The generator now re-emits those lines at their historical position, writes LF, and sorts the directory scan so output is byte-stable across filesystems.
… restart The split light/dark stage exposed two ways a nested .dark subtree was only a paint-over: 1. color is an inherited property whose computed value locks at <body> (light), so any component without an explicit text color kept rendering light-scheme text on the dark column. The column now carries text-foreground + color-scheme: dark itself, restarting the inheritance chain inside .dark. 2. reka portals teleport to <body>, escaping the .dark scope entirely, so menus/tooltips/dialogs opened from the dark column stayed light. New src/lib/portal.ts provides an injection key + usePortalTarget(); every overlay wrapper (Select, DropdownMenu, ContextMenu ×2, Popover, HoverCard, Tooltip, Dialog ×2, Sheet) forwards it as the portal's :to, and the stage's dark column provides a body-level .dark container — body-level so floating-ui positioning, fixed overlays and overflow scrolling are unaffected. Default (no provider) still falls through to <body>, so app behavior is unchanged.
Sharing ONE live state across both split columns was the architecture bug behind 'click the light trigger, the menu opens in the dark column': a controlled open v-model gets arbitrated between two reka roots, so only one overlay could ever exist and it rendered on whichever root won. The same fight broke hover-pinned tooltips. New model in ComponentPage: a canonical state (rail + code panel) plus one state per column. Config keys sync two-way through the canonical state; open is per-column local — each trigger owns its overlay, overlays can coexist, and the rail's Open switch is a pin/clear-both command rather than a shared v-model. Examples/matrix walls build fresh frozen states per column for the same reason. Non-split single mode is unchanged (binds canonical directly). CanvasStage passes column/split via slot props; PortalScope stays — portal redirection is reka's official scoped-theme mechanism, it was the shared state underneath that was wrong.
…ayer is a singleton Two open reka overlays cannot coexist: DismissableLayer is document-level, so the body inert layer strips pointer-events from whichever overlay opened first and its menu can't be hovered. The portal-redirection + per-column-state work in 5f678de/019a57f was papering over that primitive constraint, not fixing it — so it's reverted wholesale: - remove src/lib/portal.ts + showcase PortalScope + the :to wiring on all 10 overlay wrappers (restored to their pre-portal source) - remove per-column split state from ComponentPage (back to single live state) - CanvasStage gains canSplit; ComponentPage passes !isOverlay, so overlay pages (open control) simply don't show the compare toggle Kept as independently-correct: the dark column carries text-foreground + color-scheme:dark (needed for NON-overlay compare too — color inheritance locks at <body>), and the tooltip comment fix (--tooltip stays dark in both schemes). To view an overlay in dark, flip the whole page theme.
…ad-lock
The Examples wall pinned Select open:true, but the pin wrote into the throwaway
state that render() rebuilds every call, so onUpdate:open went nowhere — the
menu could never close, and its DismissableLayer froze the whole page's
pointer-events (Single unclickable, canvas wedged). Same root cause as the
Overview auto-Delete-dialog and the Dialog-page auto-open reported earlier: a
controlled overlay pinned open on a non-live instance.
These components are uncontrolled by nature (reka trigger opens, Esc/outside
closes). So the showcase now renders them that way — a closed, live trigger you
click, exactly like the real component and the honest copy-paste demo:
- spec gains ; the 5 overlay specs set it and drop their
control + every open/onUpdate:open binding. Dialog/Dropdown/Popover
use their real Trigger (+ DialogClose/PopoverClose to close).
- Tooltip keeps a resting-open preview via reka's UNCONTROLLED defaultOpen (no
scrim, can't wedge the page), suppressed under state.__preview so Overview
thumbnails stay calm.
- matrix dropped from tooltip/popover (can't freeze an uncontrolled trigger
open per cell); ComponentPage keys isOverlay off spec.interactive and drops
the dead open-pin.
Verified in-browser: Examples/Single/Dialog/Dropdown/Popover all load closed,
body pointer-events stays auto, click opens and Esc closes, Overview sprouts
zero overlays.
…hed stages A component page was a pure workbench: one stage with three mutually exclusive modes (single / examples / matrix), a bottom code bar, and the description + usage buried in the rail and a tab. Any given moment showed exactly one state, and the modes that could show many (examples wall, matrix) had no semantic organization. The page is now a vertical doc spine with the workbench embedded in it: header (name + description + imports line) -> Playground (the live instance the controls rail still drives, with its live snippet) -> one section per example (title + optional one-line note + instances frozen at preset state + collapsible View Code) -> All variants (the matrix, when declared) -> Usage. All example sections render at once down the scroll — the density a single stage could never have — while the rail stays put and keeps the Playground controls at hand anywhere on the page. - ExampleSpec gains optional note/noteZh (one line of when/why, never a restated title); ComponentSpec gains optional imports. Both backward compatible; button.ts is the exemplar. - Loading a preset into the Playground moved from the rail's example selector to a per-section tweak action, offered only on examples without a render override (an override composition can't be expressed by the controls). The rail's Example list is now anchor navigation. - CodePanel (bottom bar with Code/Usage tabs) retired; ViewCode.vue is the per-section collapsible snippet, default-open on the Playground. - Overlay invariants unchanged: interactive specs stay uncontrolled, opt out of the light/dark split, and never declare a matrix.
…ght rail First visual pass on the doc spine found the fusion half-done: the rail still duplicated the page (an Examples list that the doc sections made redundant), the playground snippet stayed open by default, and the header carried an import line nobody needs in an internal library. - The rail is gone; its toggle in the tab bar and shellState.controlsOpen go with it. The page spine is now exactly: intro → inline control grid → playground canvas → example sections → All variants → Usage. - ControlsPanel is an inline grid cell layout (label over widget). Short enums render as the library SegmentedControl instead of vertical row lists, so the enum display option is renamed 'radio-list' → 'segmented' (mechanical rename across 18 specs). - All snippets start collapsed, playground included. - The spec imports field is removed (dead API, one use). - Button spec: icon sizes now render an icon child + aria-label instead of a text label that overflowed the square box — visible in the All variants grid, which crosses every variant with the icon sizes. - All variants section scrolls sideways when the grid outgrows the measure; column headers no longer wrap mid-name.
The inline grid (label-over-widget cells, sm-size widgets) invented a control shape the design system doesn't have — cramped, low-density, and exactly the same-shape-different-code drift the owner vocabulary exists to prevent. The showcase can't import the host's SettingsSection/ SettingsRow, so ControlsPanel now transcribes their canonical geometry: --radius-menu-shell bg-card shell, mx-4 min-h-[3.75rem] py-3 rows with an inset hairline, label left (text-control font-medium), trailing control at ml-4 shrink-0, widgets at default sizes. Also drops the per-section "tweak in Playground" CTA — the controls are one scroll up; an extra action per section header was noise. AGENTS.md records the decree: never invent a third control-board layout.
…spacing Three review findings: - Snippets removed entirely. A "Code" disclosure under every section read as noise and duplicated what render() already shows — so the per-spec code() contract goes with it: 46 snippet functions across 23 specs, the codegen helpers, ViewCode/CodeBlock/highlight, and the schema fields. AGENTS.md records the decree: if a copy-paste story is ever needed it returns as ONE designed surface, not per-section disclosures. - Button "Loading modes" example spun a RefreshCw glyph (loadingMode "icon" animates the slotted leading svg). The loading glyph is always the arrow-less Spinner — a spinning action icon is never a loading state. The example now shows overlay + leading only, and the note states the rule. NOTE: the library still SHIPS loadingMode "icon" (spin-the-slotted-icon) and the old dev wall demos it with RefreshCw — whether the mode should be deprecated is a contract decision left to the maintainer. - Section rhythm was off-vocabulary (gap-12 page column, ad-hoc mb-1/3/4). Now the contract's rungs: gap-6 between page-level sections (the host's page rhythm), gap-3 inside a section (title → note → body). Also refreshes the Overview intro copy, which still promised live code snippets and a code panel.
The showcase's control board was transcribing the host's settings-row geometry — a lookalike copy that would drift the moment the real one changed. The dependency direction made "just import it" impossible (packages/ui can't import apps/web), so the owners move DOWN into the library: src/components/settings now holds SettingsRow and SettingsSection, token-adapted (text-sm → text-control, text-xs → text-body, text-[13px] → text-label, rounded-[var(--radius-menu-shell)] → rounded-menu-shell). Pixel-identical to the host originals. The host's settings/row.vue and section.vue become re-export shims (in the shell repo) so the 64 existing call sites keep working untouched; migrating them to import from '@felinic/ui' is a follow-up. The showcase ControlsPanel now composes SettingsSection + SettingsRow for real. ui-owners SKILL.md and the showcase AGENTS.md section point at the new home.
…grid First alignment pass on the lifted SettingsRow/Section revealed the real gap: the showcase page hand-styled its section titles (text-title semibold, flush with the card), while the owner vocabulary puts titles on a px-2 text column and surfaces flush — PageShell, SettingsSection and SectionGroup all inset their titles px-2. Hand-aligning a title with its card is the same transcription sin one level up. SectionGroup (foreground label + hint, bare body) moves into the library next to SettingsRow/SettingsSection; the host's section-group/index.vue becomes a re-export shim. The doc spine now composes it for every section — Playground, examples (their note maps to the group's description), All variants, Usage — so the title tier, the px-2 inset, and the title-to-body rhythm all come from one implementation. Page intro insets px-2 to join the same text column; page-level section gaps match the host's settings-page rhythm (gap-8).
…ted tier Same-level comparison against the host's Appearance page exposed another tier violation: the doc spine's section titles rendered foreground while SettingsSection titles (Interface, Typography) are muted. The vocabulary simply didn't cover "muted label + bare body", so the composition made a local judgment call — the same failure shape as the hand-written grid and the transcribed row before it. SectionGroup gains tone: 'foreground' | 'muted' (default foreground — host pages unchanged). Every doc-spine section uses muted. The page intro's description moves to text-control, PageShell's subtitle rung. AGENTS.md gains the systemic rule: every visual decision (layout, color, type rung, inset) must trace to an owner or a documented tier; the only legal moves for an uncovered shape are extending an owner with a prop or adding a new owner. Approximating by hand is reward hacking — it passes self-review until someone does a same-level comparison.
The px-2 title inset exists to offset a title from the CARD beneath it. Sections with a bare body (text + buttons, no card) kept the inset, so the title hung 8px right of its own content with nothing to offset. SectionGroup gains `bare`: title, hint, and body share one flush edge, and the title→body gap grows (4 vs the card rhythm's 2.5) to give the bare section the air the card would have provided. The doc spine's example/All-variants/Usage sections set it; Playground (controls card + canvas) keeps the inset.
Each example's bare button/icon row now sits in a plain rounded frame — centered content, no viewport/theme chrome (that belongs to the Playground alone). With a surface beneath them these sections are no longer bare: the title takes the card-relative px-2 in 10000 set again. All variants and Usage stay bare (no card).
stages, heading-tier section titles Four review findings: - Theme/scheme state never survived reload: persist() ran synchronously right after startViewTransition SCHEDULED its callback, so the value written was always the PREVIOUS one. The swap is now instant (the host's own rationale: the control already animates its feedback; a crossfade freezes the page, swallows hover, and captured the open scheme dropdown in the old snapshot — the "weird animation" was the menu morphing inside a page-wide crossfade). persist now runs after the mutation, so state actually sticks. - Mini playground content left-aligns (a reading column, not a hero); All variants gets the same frame (no longer bare). - SectionGroup gains `heading`: the doc-page section tier — the title/hint pair mirrors the page intro's pattern (text-title semibold + text-control muted). User call: on a doc spine, chapter-style visual hierarchy beats same-level consistency with settings-page labels. All doc-spine sections use it; the frame class is a single shared constant.
…ctions The previous "heading" tier was a second, hand-rolled implementation of the intro's title+subtitle pattern — at a different rung (text-title vs text-heading), so sections visibly didn't match the page header they were supposed to mirror. Looks-alike is not reuse. PageHeader (src/components/settings) is now the ONE title + subtitle pair: text-heading semibold title, text-control muted subtitle at mt-0.5 (PageShell's block in library rungs), with a semantic level prop (h1 for the page intro, h2 for sections). The ComponentPage intro and every SectionGroup in heading mode compose it — the same component, the same pixels.
…rame Same-level comparison against the host's Plugins page proved the intro was still a lookalike: my page invented its own gutter (px-8 vs px-6), dropped the min-h-9 title row and truncate, and spaced the intro 32px from the first section where PageShell owns 24px (mb-6). Another transcription, however careful. PageShell moves into the library next to PageHeader, and its title block now composes PageHeader (which gains the page-level furniture as an explicit framed mode: min-h-9 title row, mb-6; plus an inset prop for bare sections). One pair implementation, three consumers: PageShell, SectionGroup heading, and — directly — nothing else. The showcase component page is framed by PageShell itself, so its title block, gutter, and header rhythm are identical to the host's settings/plugins pages by construction. The host's page-shell/index.vue becomes a re-export shim; call sites untouched.
…etry, menu vocabulary, one frame tier - SettingsSection: restore the host's full-bleed footer hairline (the lift had silently redesigned it to an inset one; every host caller via the shim inherited the change) - SideNav: consume the library's menuLabelClass instead of re-typing it (the old pt-3/first:pt-1 was dead code — every group label is its container's first child, so the rendered rhythm has always been py-1) - ColorStack: replace the hand-rolled popover-style hover bubble with the library Tooltip component (deliberate visual delta: flat dark pill) - lib/frame.ts: one home for the staged-surface tier (rounded-lg border-border-soft), consumed by ComponentPage's stages + playground frame, ColorStack, and the overview cards
- Playground drops from the PageHeader tier to the settings-section label tier (SectionGroup tone=muted — the 13px muted label, same as the host tool-approval page's 'Server Workspace'); functional sections and doc chapters never share a heading tier - Usage prose moves from text-body (a caption rung) to the description rung text-control — 12px is never long-form copy - AGENTS.md records the tier law so the next page can't re-mix them
…nger shift the column A page with no examples/matrix/usage is shorter than the viewport and shows no scrollbar; without a reserved gutter its content column renders wider than scrollable pages, so switching pages jumped the layout sideways. scrollbar-gutter:stable on both scroll containers (App.vue static main + ComponentPage doc column) makes page geometry identical regardless of content height.
The package-level AGENTS.md is the entry point for everyone doing Memoh frontend work — including host-app developers who never touch the showcase. The showcase doctrine (doc spine, tier law, overlay invariants, spec rules) now lives in showcase/AGENTS.md per the repo's local-overrides- parent convention; the parent keeps only a pointer. README's reference updated (and its stale 'live Vue code generation' line dropped — snippets were removed).
… itself - PageShell gains the width ladder (md/lg/xl): the measure is legislated, never a hand-set max-w/p-8 (host default unchanged) - SpecimenTable/SpecimenRow: the hairline token-ladder list gets one home (was hand-written 8 times across 6 pages) - Overview + all 8 foundations pages: PageShell frame + SectionGroup chapters (heading/bare by body shape); loose notes become SectionGroup descriptions; prose rises from text-body (caption rung) to text-control - One-off demo figures (elevation pairs, spacing bars, layer stack, icon grid) keep their classes with comments stating why they are not shared vocabulary — the showcase is the copy-precedent surface, so every class must trace to an owner or justify itself - showcase/AGENTS.md legislates the copy-precedent rule and the static-page law
RowButton (sidebar nav, overview board, control lists) carried no weight class, so rows fell to normal (360) — 70 units under the host's settings nav, which is why the sidebar text read as thin and 'somehow wrong'. The host's 430 is a waived arbitrary value; the contract's answer is the medium role rung (450). AGENTS.md typography section now legislates: waived arbitrary weights map to the nearest rung, and unweighted label text is a bug class of its own.
… owner The showcase sidebar transcribed the host's settings nav (and badly: 360 weight vs 430, full ink vs /85, no tracking). Per the file-move doctrine the host's settings-sidebar/nav-item.vue is now lifted verbatim into src/components/settings — weight 430, tracking -0.1px, ink foreground/85 travel as PINNED owner values (ui-allow-alpha marker; the weight-law clause in AGENTS.md now distinguishes owner-pinned values from call-site free-styling). The host file becomes the 5th re-export shim; the showcase SideNav consumes NavItem and the orphaned RowButton is deleted. Import is relative (../button) — the #/ alias doesn't resolve when the host type-checks library sources through a shim.
Not just the row: the panel recipe is now the host settings sidebar's — 240px (was 256), px-4 list inset (was 8px), compact SidebarGroupLabel (the same component the host's settings sidebar uses, replacing menuLabelClass), gap-1 row seams (was 2px), NavItem rows. The wordmark lands on the same 30px text edge as the rows; the footer follows the px-4 inset. The only deliberate difference from the reference: these rows carry no icons.
… library The host's spacing/form skeleton owners move into the library (one implementation, two consumers — host pages and the showcase), each lifted byte-verbatim with only library-boundary adaptations (token renames, relative imports, ui-allow markers for pinned values): - settings/: FieldStack, FormStack, ExpandableSettingsRow, MetricReadout, BackButton, StatusDot, SettingsShell, ModelListRow, SwapTransition, BackendCard, DetailPane (+ width.ts — the page-width ladder as single source; DetailPane's copy lacked the 'full' rung) - component dirs: FormDialogShell, InlineLoadingRow, PanePlaceholder, PersonaTile, CalloutBanner, DiffTitleBar, DeviceCodePanel, ConfirmDeleteDialog, ConfirmPopover - lib/clipboard.ts (useClipboard); gen-entry emits its export - style.css: the swap-forward/swap-back transition classes move here from the host (their owner moved) - i18n policy: DeviceCodePanel / ConfirmDeleteDialog / ConfirmPopover now take all copy via props (library holds no i18n) - PersonaTile disabled opacity normalizes 60→40 (the contract hard-fails non-40 disabled opacity — host visual delta: dimmer disabled tile) - ui-owners skill: blanket shim note + entries for the 7 previously unregistered owners (SettingsShell, DetailPane, BackButton, SwapTransition, StatusDot, FormDialogShell, ConfirmPopover) + ConfirmDeleteDialog/DeviceCodePanel prop corrections
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0952acceb9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| v-model="viewport" | ||
| :can-split="!isOverlay" | ||
| > | ||
| <component :is="playgroundBody" /> |
There was a problem hiding this comment.
Render generated VNodes through a render component
Vue's dynamic <component :is> expects a component definition or registered name, but playgroundBody is an already-created VNode. Vue therefore treats the VNode object as a component definition with no render function, leaving the Playground blank; the same mistake affects example and matrix bodies below. Return the VNode from a small functional/render component instead so the required component-page spine actually displays its specimens.
AGENTS.md reference: showcase/AGENTS.md:L41-L46
Useful? React with 👍 / 👎.
| descriptionZh: | ||
| '面板切换器,指示器按激活项实测滑动。underline 是通栏区块导航栏(默认);pill 是内嵌于卡片等表面、不适合底部轨道的封闭铬。', | ||
| controls: [ | ||
| { kind: 'enum', key: 'variant', label: 'Variant', options: tabsListVariantKeys, default: 'underline', display: 'segmented' }, |
There was a problem hiding this comment.
Remove the pill variant from the Tabs showcase
Including pill in the showcase's variant control and matrix makes the living copy-precedent explicitly teach consumers to re-skin Tabs as an enclosed selector. That directly reverses the repository's Tabs/SegmentedControl distinction and will reintroduce the ambiguity the contract was written to prevent; showcase Tabs only with the underline treatment.
AGENTS.md reference: AGENTS.md:L187-L189
Useful? React with 👍 / 👎.
| submitText: string | ||
| submitDisabled?: boolean | ||
| loading?: boolean | ||
| maxWidthClass?: string |
There was a problem hiding this comment.
Replace maxWidthClass with an enumerated width prop
Exposing maxWidthClass in this new public component makes every caller responsible for knowing and copying an appearance-class recipe, so dialog widths can drift and future token/component changes cannot update consumers centrally. Define a finite width ladder such as width="sm" | "md" and translate it inside FormDialogShell, rather than accepting arbitrary classes.
AGENTS.md reference: AGENTS.md:L66-L69
Useful? React with 👍 / 👎.
| watch(() => props.direction, (dir) => { | ||
| if (!scrollEl) return | ||
| if (dir === 'forward') scrollStack.push(scrollEl.scrollTop) |
There was a problem hiding this comment.
Capture scroll when the pane changes, not direction
When a forward navigation occurs while direction is already 'forward'—including an initial forward default or two consecutive drill-downs—this watcher does not run, so the current list offset is never pushed. The later back transition consequently pops an empty/stale stack and returns the parent pane to the top instead of its previous position; tie capture to the actual entering/leaving pane key rather than requiring the direction value to change.
Useful? React with 👍 / 👎.
Summary
The repo's Storybook was idle and drifting. This PR replaces it with a self-built, spec-driven showcase — a living reference for the design system, built entirely from the library's own components and contract, served by
pnpm devinpackages/ui.Component pages are a doc spine. The page frame is PageShell itself (lifted into the library — see below); inside it, one vertical scroll: Playground → one chapter per example → All variants (declared matrix only) → Usage. Live instances down the whole page; no code snippets, no stage-mode switcher, no right rail.
*Keys(single source, never hand-copied lists).open(reka's DismissableLayer is a document-level singleton; three prior dead-locks). No light/dark split, no matrix for overlays; Tooltip seeds via uncontrolleddefaultOpenand is suppressed in Overview thumbnails.scrollbar-gutter: stableon both scroll containers).The entire settings/form owner vocabulary moves into the library — moved file-by-file (verbatim, token-adapted only:
text-sm→text-control,text-xs→text-body,text-[13px]→text-label,rounded-[var(--radius-menu-shell)]→rounded-menu-shell,@felinic/uiimports → relative; each move byte-diffed against the host original):src/components/settings/: PageShell, PageHeader, SectionGroup, SettingsSection, SettingsRow, FieldStack, FormStack, ExpandableSettingsRow, MetricReadout, BackButton, StatusDot, SettingsShell (+ sharedsettings/width.tsladder), ModelListRow, SwapTransition, BackendCard, DetailPane, NavItem.src/components/: FormDialogShell, InlineLoadingRow, PanePlaceholder, PersonaTile, CalloutBanner, DiffTitleBar, DeviceCodePanel, ConfirmDeleteDialog, ConfirmPopover.src/lib/:useClipboard.The library holds no i18n: DeviceCodePanel / ConfirmDeleteDialog / ConfirmPopover take all copy as required props (host call sites pass
$t(...)— prop-coverage audited 100% at every call site). Pinned host values (font-[430], /85 alpha, trackings) live only inside the lifted owners withui-allow-alphamarkers. One deliberate visual delta: PersonaTile's disabled opacity normalized 60→40 (the contract guard hard-requires 40, no escape hatch). The host keeps re-export shims (felinics/Memoh#839), so every host page and the showcase compose the SAME implementation — pixel-identical by construction, not by transcription.Shell: collapsible sidebar + content tab bar (page title, real back/forward history); en/zh locale, light/dark, 5 color schemes — all persisted. The sidebar IS the settings si 8000 debar (NavItem + full host geometry). The shell dogfoods the library throughout (Select, ScrollArea, Tooltip, the exported menu vocabulary); the staged-surface frame tier has one home (
showcase/lib/frame.ts).Foundations pages: Colors (every swatch measured live from the cascade, alpha-composited before picking label ink; the library Tooltip shows token + resolved value; click copies), Typography, Spacing, Radius, Elevation, Motion, Layers, Icons.
23 component pages, one
showcase/specs/<name>.tsper component (13 Reference + badge, card, tabs, accordion, skeleton, dialog, popover, dropdown-menu, tooltip, sonner).Contract additions (no runtime change): the owner vocabulary above; exported axis-key arrays for badge, accordion, tabs, dropdown-menu, tooltip, popover, sonner; the root barrel also exports the menu vocabulary (
menuLabelClasset al).Storybook removed: 3 stories,
.storybook/, 3 devDeps (−1392 lockfile lines). Added 2 dev font deps:@fontsource-variable/inter,misans-vf(both already host-app deps).Docs:
AGENTS.mdstays the pure design-language contract; showcase doctrine (doc spine, tier law, overlay invariants, spec rules) lives inshowcase/AGENTS.md, so host-app developers never wade through it.Test plan
pnpm type-check(vue-tsc --build) green;eslint showcasegreen;vite buildgreenscripts/check-ui-contract.mjsgreen (alpha ratchet caught and pinned the NavItem /85 case)#/alias TS2307, identical count on main); full vitest 886/886 green (suite caught two partialvi.mock('@felinic/ui')total-replacement mocks that swallowed newly-lifted components — fixed with importOriginal spreads)pnpm dev→ Button page, Colors page, Dialog flow