8000 feat: spec-driven component showcase (replaces Storybook) by qqqqqf-q · Pull Request #6 · felinics/ui · GitHub
[go: up one dir, main page]

Skip to content

feat: spec-driven component showcase (replaces Storybook) - #6

Merged
qqqqqf-q merged 52 commits into
felinics:mainfrom
qqqqqf-q:feat/showcase
Jul 26, 2026
Merged

qqqqqf-q merged 52 commits into
felinics:mainfrom
qqqqqf-q:feat/showcase

Conversation

@qqqqqf-q
@qqqqqf-q qqqqqf-q commented Jul 20, 2026 •
Copy link
Copy Markdown
Member

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 dev in packages/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.

  • The Playground is a functional section at the settings-section label tier (13px muted label); its controls board composes the library's SettingsSection/SettingsRow — default-size widgets, options from the component's exported *Keys (single source, never hand-copied lists).
  • Chapters (examples / All variants / Usage) use the shared PageHeader — the same component PageShell's title block composes. The two heading tiers never mix.
  • Overlay specs render uncontrolled — a closed, live trigger you click — never pinned open (reka's DismissableLayer is a document-level singleton; three prior dead-locks). No light/dark split, no matrix for overlays; Tooltip seeds via uncontrolled defaultOpen and is suppressed in Overview thumbnails.
  • Page geometry is identical regardless of content height (scrollbar-gutter: stable on 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/ui imports → relative; each move byte-diffed against the host original):

  • src/components/settings/: PageShell, PageHeader, SectionGroup, SettingsSection, SettingsRow, FieldStack, FormStack, ExpandableSettingsRow, MetricReadout, BackButton, StatusDot, SettingsShell (+ shared settings/width.ts ladder), 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 with ui-allow-alpha markers. 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>.ts per 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 (menuLabelClass et 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.md stays the pure design-language contract; showcase doctrine (doc spine, tier law, overlay invariants, spec rules) lives in showcase/AGENTS.md, so host-app developers never wade through it.

Test plan

  • pnpm type-check (vue-tsc --build) green; eslint showcase green; vite build green
  • Host scripts/check-ui-contract.mjs green (alpha ratchet caught and pinned the NavItem /85 case)
  • Host repo: production build green; type-check zero new errors (471 pre-existing #/ alias TS2307, identical count on main); full vitest 886/886 green (suite caught two partial vi.mock('@felinic/ui') total-replacement mocks that swallowed newly-lifted components — fixed with importOriginal spreads)
  • Every lifted file byte-diffed vs the host original (token/import adaptations only)
  • Human visual pass on the host app against real data (parallel stack, same DB): settings, providers → model detail, bot tabs, delete confirms, popovers — no rendering regressions
  • Reviewer spot-check: pnpm dev → Button page, Colors page, Dialog flow

qqqqqf-q added 23 commits July 17, 2026 01:14
- 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.
qqqqqf-q added 6 commits July 20, 2026 07:32
…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.
qqqqqf-q added 23 commits July 21, 2026 08:09
…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
@qqqqqf-q
qqqqqf-q marked this pull request as ready for review July 26, 2026 09:54
@qqqqqf-q
qqqqqf-q requested a review from a team July 26, 2026 09:54
@qqqqqf-q
qqqqqf-q merged commit 75ebab1 into felinics:main Jul 26, 2026
1 check passed
@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread showcase/specs/tabs.ts
descriptionZh:
'面板切换器,指示器按激活项实测滑动。underline 是通栏区块导航栏(默认);pill 是内嵌于卡片等表面、不适合底部轨道的封闭铬。',
controls: [
{ kind: 'enum', key: 'variant', label: 'Variant', options: tabsListVariantKeys, default: 'underline', display: 'segmented' },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +59 to +61
watch(() => props.direction, (dir) => {
if (!scrollEl) return
if (dir === 'forward') scrollStack.push(scrollEl.scrollTop)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0