feat(scraps): Adopt SplitPanel with composable API redesign#116274
Draft
priscilawebdev wants to merge 8 commits into
Draft
feat(scraps): Adopt SplitPanel with composable API redesign#116274priscilawebdev wants to merge 8 commits into
priscilawebdev wants to merge 8 commits into
Conversation
Move `SplitPanel` from `sentry/components/splitPanel` into `@sentry/scraps/splitPanel`. Adds the standard scraps companions — `index.tsx` barrel and `splitPanel.mdx` stories — and updates the two existing consumers (the conversation layout and the replay panel wrapper) to import from the new alias. The Replay wrapper no longer wraps `setStartPosition` in `useCallback`; scraps components are intentionally unmemoized, so the wrapper provided no benefit and ESLint's `@sentry/no-unnecessary-use-callback` rule flagged it. Switches `handleResize` from `useCallback` to `useMemo` so the debounced function reference stays stable across renders without tripping the same rule. Tightens the divider `onMouseDown` event type from `any` to `React.MouseEvent<HTMLElement>` so the moved file passes the stricter type-aware rules applied inside `static/app/components/core/`. Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
📊 Type Coverage Diff
🔍 4 new type safety issues introduced
Type assertions (
This is informational only and does not block the PR. |
Trade the old prop-config API (`left={{content, default, min, max}}` /
`right=...`, plus an `availableSize` prop the caller had to thread in)
for a composable shape:
<SplitPanel orientation="horizontal" sizeStorageKey="...">
<SplitPanel.Panel defaultSize={300} minSize={200} maxSize={600}>
{left}
</SplitPanel.Panel>
<SplitPanel.Divider />
<SplitPanel.Panel>{right}</SplitPanel.Panel>
</SplitPanel>
The root now self-measures with `useDimensions`, so callers no longer
have to thread the container width or height in. The pane that
declares `defaultSize` is the sized pane; the other fills.
Adds a focusable `<SplitPanel.Divider>` with `role="separator"`,
`aria-orientation`, `aria-valuemin/max/now`, and arrow-key resize
(Shift for coarse step, Home / End to snap to bounds). Exposes a
`useSplitPanelDivider` hook for consumers that need a custom divider
visual (e.g. the conversation layout's 1px border line) -- the hook
returns the ARIA props, event handlers, and the `isHeld` state.
Migrates both consumers:
- The conversation layout swaps its `BorderDivider` prop to a small
component that uses `useSplitPanelDivider` and renders the line via
`border-left` (background tokens cannot reference `border.*` per
the scraps token rules).
- The Replay layout composes the two panes as children. The
`ReplaySplitPanel` wrapper keeps its tracking responsibility but
now accepts `children` + `orientation` + `availableSize` instead of
the old prop bag, so the layout file builds the panes once instead
of constructing two divergent prop objects.
Co-Authored-By: Claude <noreply@anthropic.com>
`SplitPanel` is a layout primitive; persistence is behavior and composes from `defaultSize` + `onResize`. Chakra's Splitter takes the same line -- no built-in storage prop -- so this matches the precedent for splitter primitives in modern design systems. The conversation layout (the only consumer that used the prop) now calls `useLocalStorageState` itself and threads the value in via `defaultSize` / `onResize`. ~3 lines extra at the call site, no behavior change. Co-Authored-By: Claude <noreply@anthropic.com>
Switch the default `<SplitPanel.Divider>` from a grab-handle (with `IconGrabbable`) to a thin 1px line, matching the trace-drawer / conversations house style. The line uses a wider invisible hit area (`::before`) for comfortable dragging, and changes color on hover and while held via `border.accent.moderate`. Conversations no longer needs its custom `BorderDivider` -- the default now renders the same look, so it drops the custom component and uses `<SplitPanel.Divider>` directly. The `useSplitPanelDivider` hook stays exported for consumers that genuinely need a different visual (e.g. a grab-handle in a feature where extra affordance is wanted) -- documented in the mdx with a worked example. Also makes `SplitPanelRoot` self-fill (`height: 100%; width: 100%`) so the component works in block parents too. The mdx demos wrap the SplitPanel in `<Flex>` instead of `<Container>` so the demos render at the full container height, and both panes in the demos now use `background="primary"` for a uniform look. Co-Authored-By: Claude <noreply@anthropic.com>
Matches Zag/Chakra's splitter: `maxSize` defaults to "the container size" (100% in their percentage model), so a panel can never grow beyond its parent. We were defaulting to `Infinity`, which let `useResizableDrawer`'s drag accumulate past the container and produce overflow glitches. Explicit `maxSize` still wins -- the cap is `min(maxSize, availableSize)` once the container is measured. Pre-measurement, the cap is just the explicit max so the hook can accept the initial size without clamping it to zero on the very first render. Also drops the artificial `maxSize` from the mdx demos so the user can drag the sized pane to fill the container, instead of bottoming out at an arbitrary demo value. Co-Authored-By: Claude <noreply@anthropic.com>
Container is 600px; pick defaultSize=300 so both panes start equal. Co-Authored-By: Claude <noreply@anthropic.com>
Both the horizontal and vertical demos now start at 50/50 and render a `Left: X% | Right: Y%` (or `Top` / `Bottom`) line that updates as the user drags the divider. Demonstrates `onResize` end-to-end: SplitPanel passes the new pixel size; the demo divides by the known container size and re-renders the indicator. Each demo is a local `export function` in the mdx so the state lives inside the component -- no separate demos file needed. Co-Authored-By: Claude <noreply@anthropic.com>
`useResizableDrawer` only enforces `min`, not `max`, so its internal size could drift past the cap while the user kept dragging -- and `onResize` fired with that raw out-of-range value. The visible panel stayed at the cap (because the component renders `min(size, max)`), but consumer callbacks saw values exceeding `availableSize`, which is why the demo's `Right: Y%` indicator could flip negative. Wraps the hook's `onResize` to clamp before forwarding, and snaps the hook's internal state back when it drifts. Snapping back also fixes a UX quirk: previously, dragging far past the cap and then back didn't produce visible motion until the cursor caught back up to the drifted internal value. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adopts
SplitPanelinto@sentry/scraps/splitPaneland redesigns its API in the same change. The old prop-config shape was clunky enough at the call site (every caller had to threadavailableSizein by hand and pass aleft/right/top/bottomconfig object) that promoting it as-is would have locked the suboptimal shape into the design system. Better to redesign now, while there are only two consumers.Two precedents in the same vein: #113489 (
feat(scraps): adopt GlobalDrawer component into design system) collapsed boolean props intomodeand dropped unused overrides during its promotion, for the same reason.The new API
<SplitPanel.Panel>and<SplitPanel.Divider>as subcomponents instead ofleft={{...}} right={...}prop bags.useDimensionsinternally. Callers drop theuseRef+useDimensions+width > 0gate pattern.<SplitPanel.Divider>renders withrole=\"separator\",aria-orientation,aria-valuemin/max/now, and arrow-key resize (Shift for a coarser step, Home / End to snap to bounds).useSplitPanelDivider— for the 1px-line look the conversation layout wants, the consumer renders its own styled element and spreadspropsfrom the hook (ARIA + event handlers +tabIndex+role).useSplitPanel— descendants can callmaximiseSize/minimiseSize/resetSizeand readisMaximized/isMinimized.Scope decisions
minSize/maxSize. Standard separator a11y pattern.availableSizesurvives only as an internal detail of theReplaySplitPanelwrapper, where it's used to compute the resize end position as a percentage for analytics. The Replay layout already measures itself for its own grid, so threading the measurement avoids a redundant seconduseDimensions.Verification
pnpm lint:js(touched files): cleanpnpm typecheck: cleanpnpm test-ci splitPanel.spec.tsx: 6 / 6 (covers horizontal and vertical orientations, collapse-to-fill behavior with DOM-identity preservation across rerenders, and the divider's ARIA attributes)