Skip to content

fix(ui): harden registry components against worst-case data - #52

Merged
yyyyaaa merged 19 commits into
mainfrom
phat/break-ui-storybook
Oct 13, 2026
Merged

yyyyaaa merged 19 commits into
mainfrom
phat/break-ui-storybook

Conversation

@yyyyaaa

@yyyyaaa yyyyaaa commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Hardens every @constructive-io/ui component in Storybook against realistic worst-case data: long, unbreakable names and emails, translated labels, large counts, empty and single-item lists, RTL text and 200% font size. Adds a Storybook harness so these cases stay reproducible.

Harness

  • A Dataset switch (Demo / Worst case / Empty / One / 1,000 rows) is pinned to the canvas and kept in the URL (&globals=dataset:worst;direction:rtl).
  • A Direction toggle wraps the canvas in Base UI's DirectionProvider.
  • src/stories/break-ui/ holds about 90 stress stories with shared worst-case fixtures (worst-case.ts).

Fixes (186 findings: 163 fixed, 6 partly fixed, 15 won't fix, 2 deferred)

  • Overlays: Dialog, AlertDialog, Sheet, Drawer, Popover and DropdownMenu cap their size at --available-height/--available-width and scroll inside. A new initial-focus helper keeps a scrolling overlay opening at its top.
  • Overflow: min-w-0 and overflow-wrap: anywhere where text sits next to actions (Item, Card, PageHeader, Field, Form errors, InputGroup, Command, Accordion, Sidebar). Tabs and Stepper scroll instead of widening the page.
  • Kits: a base grid-cols-1 on 26 kit grids that only set columns at breakpoints, and kit body text in rem instead of px.
  • Logic:
    • Progress clamps its value and treats NaN/null as indeterminate instead of drawing a full bar.
    • Select shows the label of an item declared in its children without items. Items rendered by a wrapper component still need items.
    • MultiSelect's +N more no longer clears selections. Clearing moved to the popover.
    • Plans with no active price are hidden and refused.
    • humanizeBytes rolls over at 1,048,576 bytes.
    • Chat markdown keeps literal asterisks and newlines.
  • Names: a shared initials() (grapheme-safe, first + last word) replaces seven copies that broke on emoji and CJK names.
  • Shared helpers: duplicated per-kit code moves into registry lib items: intl (cached formatters, formatCount, plural, nameList, graphemes), text-runs (one truncating span per text run), use-scroll-fade, use-document-theme (one <html> observer for Sonner and JsonInput) and json-error-position. Storage and data rooms share one MiddleTruncate.
  • RTL: logical sides on ButtonGroup and Sidebar (start/end), dir="ltr" on dial codes, OTP digits and shortcuts, and mirrored Calendar chevrons.

New optional API

<Button wrap>, <Button focusableWhenDisabled>, AlertAction, Spinner label/decorative, Select emptyMessage, JsonInput maxLines, Sidebar side="start" | "end", initials(name, { locale }), MiddleTruncate title, PlanComparison currentPrice, registry lib items initials, initial-focus, compose-refs (mergeRefs/useMergedRefs, which keeps React 19 ref cleanups), intl, text-runs, use-scroll-fade, use-document-theme and json-error-position, and kit helpers formatCount, plural, nameList, MiddleTruncate, isPlanAvailable and resolvePrice.

Behaviour changes for hosts

  • Toaster follows the page's dark class instead of system, and every toast variant clamps to 3 lines with "View details". The details dialog renders from <Toaster> beside Sonner rather than inside the toast, so it keeps the app's providers, survives the toast's timeout and swipe, and returns focus on close.
  • A disabled Button under a Tooltip stays the same element instead of being wrapped in a span: TooltipTrigger sets focusableWhenDisabled, and Button renders through Base UI's Button (aria-disabled, focusable, inert to clicks). Other disabled controls need a wrapper passed through render to get a tooltip, as on main.
  • Billing plurals choose between the English forms by English rules in every locale, while numbers still format in the host locale. Unlimited limits never reach a plural rule.
  • Dialog, AlertDialog and Sheet focus the popup itself on touch (Base UI's default) and otherwise when the first focusable element is below the fold.
  • Plans without an active price disappear from PlanComparison (the current plan stays).
  • Initials use the first and last word, so Christopher Alexander Montgomery III becomes CM.
  • Sidebar menu badges cap at 3rem and end in an ellipsis with the full value as a tooltip.
  • Storage file names keep the extension plus four characters visible at the end instead of a fixed 12. Kit counts that aren't finite numbers read "—".

Deliberately not handled here

  • Pagination, windowing and virtualization of long lists stay with the host.
  • Primary-colour contrast (theme tokens) is unchanged.
  • Kit copy translation (a messages prop) is deferred.

Before / after (same story, worst-case data)

Case Before After
Dialog, 700px, German/French footer labels: the first button is pushed off-screen, and now wraps in place before after
Drawer, 320px, 40 rows: the title and close button leave the screen, and now the drawer caps its height and scrolls before after
MultiSelect, 320px, 13 selections: the in-trigger clear badge drops selections on a centre click, and is now plain +N more text before after
FormControl, 320px, error with no spaces: the field grows to 690px, and the error now wraps before after
InputGroup, 320px, long addons: the input collapses to 16px, and now keeps a usable width before after
RecordPicker, 1440px, 13 records: maxHeight is ignored and rows spill out, and the list now scrolls at 300px before after
Tabs, 1440px, 10 tabs: a 1,944px row widens the page, and the tab list now scrolls with edge fades before after
Billing usage, 320px: the implicit grid track overflows, and a base grid-cols-1 fixes it before after
Pagination, 320px, 1,284 pages: page numbers overlap, and links now keep a minimum width before after
Item, RTL, member rows: the content column is squeezed to about 40px, and actions now wrap below the content before after

🤖 Generated with Claude Code

https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ

Adds a dataset switch (demo, worst, empty, one, 1,000 rows), a direction
toggle wired to Base UI's DirectionProvider, and worst-case stories for
every component in the Storybook.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
Fixes the break-ui findings for the primitives group (rows from the group A
report, by number):

- Progress (1): finite values clamp into the range; null, NaN, Infinity and
  unusable bounds are indeterminate with a sweeping band; data-overflow above max
- Item (2-4, 20, 40): content floor so actions wrap before the text is crushed,
  a title clamp that works, wrapping actions, min-w-0 header and footer, rem text
- PageHeader and Card (5-7, 21): wrapping title and actions, minmax(0, 1fr)
  header grid, wrapping footer, leading-tight titles
- Stepper (8, 30): shrinking items with truncating labels, aligned vertical steps
- Button (9): opt-in wrap prop. Tooltip (10, 33): focusable wrapper for disabled
  triggers, wrapping text, no empty bubble
- Empty, Field, Label (11-13, 34-36): overflow-wrap, min-w-0, a flex-row
  separator, an inline required star, logical text alignment
- Alert (14, 15, 22, 41): AlertAction slot, wrapping code, empty title, full
  colour descriptions
- Avatar (16, 25): object-cover, backing for transparent images, fallback text
  that scales with the avatar; initials() helper with a registry item
- Toaster and toasts (17, 39): theme follows the page; one shared card with a
  three-line clamp and details dialog for every variant
- Breadcrumb and Pagination (18, 27, 29, 41): wrapping crumbs, page links that
  grow with their label, mirrored chevrons, full colour links
- Badge (23, 24): truncating text, min-width for single characters
- ButtonGroup, Kbd, UnlinkButton, Separator, Spinner (19, 37, 38, 42, 44, 45):
  logical radii, nowrap keys, 24px target, visible vertical rule, decorative
  spinner option
- Dock (31, 32, 43): wrapping row, context injection, clientX, keyboard
  operable icons
- StackHeader (46): title attributes on truncated text

Left as host concerns or deferred (rows 7, 19, 21, 24, 26, 28, 38, 47, 48), and
primary colour contrast skipped by decision (row 41).

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
Fixes the group-B break-ui findings for the input and picker components.

Selection and labels
- Select shows the selected item's label instead of its raw value, read from
  the declared items (and items that mount later); `items` still wins (row 2)
- Select popup shows an empty message when it holds no option, keeps the
  selected row clear of the scroll arrows, and its trigger never exceeds a
  narrow container; truncated values get a title (rows 18, 19, 20)
- MultiSelect `+N more` is plain text and clearing lives in the popover, so a
  stray click cannot drop the selection (row 1)
- MultiSelect scrolls the keyboard-highlighted option into view, keeps the
  highlight pill on the hovered row, wraps option descriptions, announces the
  count plus the first labels, and uses singular copy for one option
  (rows 4, 17, 32, 35, 46)
- RecordPicker always searches the visible label (row 37)

Long and unbreakable text
- Chips (MultiSelect, Combobox, Tags) and option rows (Select, Combobox,
  Autocomplete, Command) wrap or truncate instead of pushing the control or
  page wide; chip fields cap their height and scroll (rows 5, 9, 10, 12, 39)
- Command rows keep their icon and shortcut; CommandEmpty is centered
  (rows 8, 29)
- FormControl and Form wrap long labels, descriptions and errors; floating
  labels stay on one line and also float for Textarea; FormMessage falls back
  to its children when an error has no message (rows 7, 11, 28, 42, 43)
- InputGroup addons shrink and truncate, icons never shrink, the input keeps
  a floor and the row cannot overflow its border (rows 6, 16)
- Input ellipsizes long values and placeholders; Textarea and JsonInput cap
  their height and scroll; JsonInput keeps its format button off the text,
  follows the page theme and reports line and column (rows 13, 14, 30, 31)
- ToggleGroup and Toggle wrap and truncate, and the attached group applies its
  corner radii and collapsed borders (rows 15, 33)

Direction, scripts and ranges
- Dial codes, shortcuts and counts are isolated left to right; InputOtp stays
  left to right and groups logically; email, url and tel inputs default to
  `dir="ltr"` (rows 23, 24)
- Phone country search matches calling codes and the number is capped at the
  longest valid length; InputOtp accepts digits from other numeral systems
  and its gaps no longer scale with the root font size (rows 25, 41, 44)
- Calendar nav buttons show their disabled state, chevrons mirror in RTL and
  the today dot stays centered (row 27)
- Switch is the size of its track with a pseudo-element touch target; a
  Slider with no range stays visible and disabled (rows 21, 40)

Not changed: rows 22 (host provides DirectionProvider), 26, 45 (below the
control's content minimum), 34, 38 (host paginates, host data), 36 (cmdk
identifies items by value; now documented). Row 3 is fixed by ScrollArea.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
FieldRow centred its checkbox or switch against the whole label, so a label
that wrapped left the control floating beside the middle line, and the
description sat under the control instead of under the label text.

The control now sits at the top of the row (beside the first line) and the
label, description and error share one column. Handed off from the inputs
group; the label overflow-wrap change they asked for was already in the
primitives commit.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
Fixes the group D rows from the break-ui report for billing-kit,
billing-account, billing-console, workspace-kit, agents-builder, data-rooms
and ai.

Layout and overflow (rows 1-4, 6-7, 9-15, 17-19, 26, 31-32, 34, 36, 46-47)
- base grid-cols-1 track at 26 implicit-grid sites; Usage no longer scrolls
  sideways on phones
- composer Send, room header sections, ViewHeader, Panel header, rail items,
  ledger, tables (name column floors, nowrap dates and money, scroll fade),
  approval, recommendation, draft and connect cards wrap instead of overflow
- account and Move-to lists scroll inside their menus
- canvas fit anchors wide content at the start edge, canvases and the day
  timeline stay LTR

Data handling (rows 5, 8, 16, 20-25, 28, 30, 33, 42-45)
- plans with no active price are hidden from comparisons and refused in the
  change dialog (isPlanAvailable), PriceTag shows Unavailable
- markdown keeps literal asterisks, line breaks and h4-h6; inline diff is a
  real LCS diff with context
- initials from lib/initials, plural forms via Intl.PluralRules with optional
  unitSingular, exact plan limits, year-aware short dates, calendar-day
  relative dates, percent never rounds up to 100%, compact numbers and
  durations roll over, zero timestamps render as a dash

Resilience and polish (rows 27, 29, 35, 37-39, 48 deferred)
- title on truncated strings, avatar and favicon fallbacks, empty sections
  left out, banner and attention name lists capped
- 367 px type sizes converted to rem, physical direction utilities moved to
  logical ones, directional icons mirror, arrow keys follow useDirection
- rows 40 and 41 are left to hosts (pagination); row 48 (kit copy i18n) is
  deferred

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
…s on one row

- the Switch is now its own 18px track (the touch target is a pseudo-element),
  so the -my-3 that cancelled its old 40px box in catalog and gift code tables
  pushed the toggles below the row text; centre them with align-middle instead
- the room header wraps its sections to a second row from 52rem of container
  width instead of 56rem, so the common 1100px desktop width keeps one row

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
…able added row

- the inbox working line (agent name and activity) wrapped nowhere, so an
  80-character agent name was cut at the thread edge; it now wraps anywhere
- the schedule card history row wraps its "Last run" toggle below the run
  cells instead of overflowing the card at phone widths
- DiffTable added rows wrap long cells instead of clipping them
- TableSurface scroller is positioned so screen-reader-only text inside a
  scrolled table stays inside it
- declare @base-ui/react on the workspace-kit and data-rooms registry items
  that now import useDirection directly

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
…se data

Fixes break-ui report rows 1-34 and 37-42 for group C (overlays, data
containers, workspace kit), found with the worst-case Storybook harness at
320px, 1440px, 200% text, dark and RTL.

Overlays (rows 1-9, 16, 18, 32)
- Sheet, Drawer, Dialog, AlertDialog, Popover and DropdownMenu cap to the room
  they have and scroll inside; long titles, descriptions and labels wrap and
  stay clear of the close button; AlertDialog actions wrap; a right drawer
  gets the same width as a left one.
- Dialog, AlertDialog and Sheet open at their top: when the first tabbable
  element is below the fold they focus the popup (new lib/initial-focus).

Layout and workspace kit (rows 10-14, 17, 28, 30, 31, 34, 39, 40, 41)
- Tabs scroll with edge fades, Accordion and Collapsible wrap, Sidebar icon
  mode scrolls, menu badges and group labels get room, side accepts start and
  end, ScrollArea honours max-height, Resizable gets a bigger hit area.
- OrgChart scales with the root font size and stacks more than eight reports
  without a team in columns (routed lines, arrow-key movement); ResponsiveDiagram
  floors its zoom; FlowZoomPanel fits wide graphs and takes labels; the
  decorative grids and blur are hardened.

Storage kit (rows 15, 19-27, 33, 37, 38, 42)
- MiddleTruncate for names and keys with the full key in the detail sheet,
  upload errors clamp and can be dismissed, humanizeBytes carries, counts take
  a locale, the name column has a floor, previews fall back, text is rem.
- TableCell gets an opt-in truncate prop.

Rows 29, 35, 36 and 43 are won't fix (platform scrollbar convention, host
paginates, consumer-owned empty states). Builds on Button wrap and lib/initials
from the primitives commit.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
MiddleTruncate split names across two flex items, so their accessible name
read with a break at the cut. Screen readers now get an sr-only copy. Also
drops the dotAll regex flag from initials so ES2017 consumers type-check.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
@tenki-reviewer

tenki-reviewer Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Review complete. No issues found — approved ✅.


The diff for this PR is empty (0-line patch), so there are no changes to review.

Reviewed commit: 20ee426

Fixes rows 1-10 of the group B react and logic audit.

Behaviour
- MultiSelect: Enter and Backspace inside the popover footer belong to its
  buttons, so the clear button works from the keyboard instead of toggling
  the highlighted option or dropping the last value (row 1)
- Select labels: the registry is a small external store rather than refs
  read and written during render, so Select compiles under React Compiler
  (rows 2, 5, 6)
  - with `items` the children are not walked and nothing registers (row 3)
  - an entry leaves when its last item unmounts; the selection keeps its
    label, so the trigger stays labelled when the list closes (row 6)
  - an item that moves from the declared children into a wrapper keeps its
    label (row 5)
- SelectValue keeps a `title` the host sets (row 7)
- InputGroupText, Toggle and ToggleGroupItem wrap each run of adjacent text
  once, so `{n}/{max}` stays one flex item instead of opening a gap between
  children; long labels still truncate (row 4)

Smaller fixes
- JSON error position: numbers and \u escapes are matched with sticky
  regexes, so a number longer than 64 characters no longer misplaces the
  reported line and column (row 8)
- RecordPicker: an inline `getRecordLabel` no longer re-runs the search on
  every parent render; the labels are compared by content (row 9)
- JsonInput: one shared theme observer and one resize listener for every
  editor, and none when the host sets the theme (row 10)

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
Audit rows fixed, High and Medium first:
- 1  PlanChangeDialog, PlanComparison and PriceTag read one resolvePrice(); the dialog names the period from the price it charges and onSelect gets the price the card shows
- 2  plural() and unitFor() pick English forms by English rules; the billing call sites stop passing the formatting locale
- 3  the unlimited sentinel (-1) never reaches the plural rules
- 4  lineDiff follows the edits with a bounded Myers search before it falls back to removed-then-added, so distant edits in a long file stay small hunks
- 5  the documents list is memoized and sorted with one Intl.Collator per room locale
- 6  avatar and favicon fallbacks also check complete/naturalWidth on mount, so an image that failed before hydration still falls back
- 7  useScrollFade watches children added or removed later
- 8  Intl objects in plural, nameList, unitFor and the data rooms formatters are built once
- 9  singularize exceptions, and only the last word of a phrase changes
- 10 duration and byte rounding at unit boundaries
- 11 MiddleTruncate splits at grapheme boundaries; tail 0 draws a plain line
- 12 markdown strong patterns stop at the next marker, so long lines take linear time
- 13 ContextRing label keeps a percent past 100
- 14 PlanComparison currentPrice for a retired price; Plan.fallback documented
- 15 unset or invalid dates are a dash (and "Undated") instead of a RangeError

Adds ai/line-diff.ts and ai/use-image-failed.ts, registered in the ai item.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
Fixes audit rows 1-7 for group C.

Overlays (rows 1, 2, 4)
- Dialog, AlertDialog and Sheet focus the popup again when opened by touch,
  so the soft keyboard stays closed: scrollSafeInitialFocus now receives
  Base UI's interaction type and returns the popup for touch, which the
  default it replaces did. A consumer's initialFocus still wins, and an
  explicit undefined counts as omitted.
- TabsList, DialogPopup and AlertDialogContent merge their own ref with the
  consumer's through the new lib/compose-refs helper (mergeRefs,
  useMergedRefs), so React 19 ref cleanups run, a cleanup ref is never
  called with null, and the merged callback keeps its identity between
  renders. Registered as the compose-refs lib item and aliased in the
  registry compiler; dialog, alert-dialog and tabs depend on it.

Data and layout (rows 3, 5, 6, 7)
- SidebarMenuBadge shares one ResizeObserver that reads every badge width
  before writing any item variable, and reads no layout at mount, so a long
  list of badges no longer forces a layout per row.
- MiddleTruncate draws text that fits in its tail as one selectable line.
  Split text keeps an sr-only copy and makes the two visible halves
  unselectable, so copying a name pastes the original string once instead
  of name, newline, tail. The Intl.Segmenter is built once and the split is
  memoized per text.
- MiddleTruncate takes a title prop (false leaves it off). BucketRail passes
  false so the row's "name, details" tooltip is no longer hidden by a nested
  title.

Each High and Medium row has a regression test that fails without the fix.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
Audit rows fixed (a-audit.md):

1, 2. toast: the "View details" dialog opens in a root of its own instead of
   inside the toast. Sonner's timer, toast.dismiss and swipe-out can no longer
   unmount it, a drag inside it is not read as a swipe, it does not hold the
   toaster in its hover state, and mouse text selection works.
3. badge: each run of string and number children is wrapped once, so
   interpolated text stays one flex item (no gap inside "{n}%" or "+{n}") and
   icon plus text still truncates. Whitespace between elements adds no item.
4, 5. tooltip, button: a disabled Button trigger is no longer wrapped. It stays
   the same element in the same box, disabled with aria-disabled (focusable,
   hoverable, ignores clicks, presses and submits), so layout is unchanged and
   nothing remounts when disabled flips. Other disabled controls are left
   natively disabled; the docs say to supply a wrapper for them.
6, 11. dock: Enter and Space press a DockIcon only when the key came from the
   icon itself, and the consumer's onKeyDown runs first and can cancel it.
7. separator: the vertical rule no longer sets align-self.
8. stepper: the horizontal scroll container keeps room for the focus ring and
   the hit-area overhang (vertical padding with a matching negative margin).
9. initials: casing no longer depends on the runtime locale; opt in with the
   new locale option.
10. toast: one copy-feedback timer, cleared and extended, never left running.
12. field: a labelled FieldSeparator is one separator to assistive tech.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
…e-group

InputGroupText, Toggle and ToggleGroupItem now use the same helper as
Badge: each contiguous run of text becomes one truncating element, elements
are left alone, and a run of only whitespace between elements is dropped
instead of being left behind as a text node.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
…commit

Two orderings could drop the label of a value that was just picked from
wrapper-rendered items:
- the selection was recorded in a passive effect, which runs after the
  cleanups of items that unmount in the same commit; it is now a layout
  effect, which runs before them
- Base UI reports `null` when it resets a value that has no mounted item,
  and the Select recorded that as the last pick; a controlled Select now
  ignores it, as its host never receives it

Both surfaced as a test that failed about one run in forty; two
deterministic cases now cover them.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
The details dialog opened in a React root of its own (cdab56f). That fixed the
toast timer and Sonner's swipe and hover handlers, but a new root does not get
the app's providers (Base UI DirectionProvider, motion settings, the portal
context, theme or i18n), and nothing gave focus back when the toast and its
"View details" button were already gone.

- openToastDetails publishes the request to a small store (on globalThis, so a
  package copy and a registry copy still find each other).
- Toaster renders ToastDetailsHost next to Sonner, not inside it: Sonner never
  sees the dialog's events, and the app's providers reach the dialog. The first
  of several toasters shows it; a host that unmounts drops its pending request.
- Without a Toaster of ours (Sonner's own Toaster) it still opens in a root of
  its own, as before; documented in the code.
- Focus goes back to the control that opened it (the toast passes its button, so
  it also works where a click does not focus a button, as in Safari) while that
  control is on the page, else to the toaster's region, via Base UI's
  finalFocus.
- Registry: toast-details.tsx moves to the sonner item (which now depends on
  button and dialog); the toast item depends on sonner.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
line-diff.ts and use-image-failed.ts joined the ai registry item, so the
expected source closure has to name them.

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
- Select reads labels from declared items only; the wrapper-item registry,
  its layout effect and picked-value state are gone (pass `items` instead)
- Shared lib modules replace parallel copies: text-runs (badge, toggle,
  toggle-group, input-group, sidebar badge), use-scroll-fade (tabs, kits),
  use-document-theme (sonner, json-input), intl (cached formatters,
  formatCount, plural, nameList, isUnsetDate, graphemes) and
  json-error-position
- One MiddleTruncate in workspace-kit replaces the storage and kit copies
- PlanChangeDialog renders an unavailable plan through its own body instead
  of guarding six branches
- TooltipTrigger sets a typed `focusableWhenDisabled` on Button, which uses
  Base UI's Button for it, replacing the data-attribute protocol
- Sidebar menu badge is capped in CSS instead of measured by an observer
- Toast variants share one ToastProps type

Claude-Session: https://claude.ai/code/session_014YjBrkY2FYbr72Cc6DpRKJ
@yyyyaaa
yyyyaaa merged commit e409e2a into main Oct 13, 2026
5 checks passed
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