feat: the tool panel — rail, pane, and the columns section (SP1) - #486
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…epair comments Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hiding a column now also cancels an open edit session addressing it — the same repair setColumns applies when the edited column leaves the layout — and beginEdit refuses a hidden target outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The drawn projection, the facade's getColumns and mergeRenderColumns all filter hidden layout entries, so every span consumer — copy, paste, selection, CSV, announcements — inherits correctness from one place. moveColumn takes a DRAWN-space index and splices hidden ids back into the engine's full roster by visible neighbor; the controlled state.columnOrder write-back accepts the drawn vocabulary and does the same splice. The full layout stays reachable via getState().columnLayout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s; drawn-vocabulary comments Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rules only — the React components land in later tasks, so these target data-pretable-tool-* attributes nothing emits yet. Four structural guards pin the surfaces (rail=header, pane=toolbar), the token contract, the no-opacity dimming rule, and the element roster; each was mutation-tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s for the row controls The selected-tab rule follows the hover rule at equal specificity, and its background SHORTHAND was resetting background-image — no hover tint on the open tab. Now background-color, with a guard pinning it (mutation-tested). The row grip and kebab get the header ⋮'s coarse-pointer ::after hit-area treatment, centred because they have no neighbouring target to steal from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reset pinned Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The surface renders [grid stack][pane?][rail] in one flex row wrapped by [data-pretable-tool-layout], which takes over the card chrome from the viewport (grid.css redistributes border/radius/shadow, the same dance the group-panel wrapper does vertically). toolPanel?: boolean | config, default true; activeSection present makes the open section controlled, else internal state seeded by defaultActiveSection. The rail tablist gets its accessible name through the surface's messages layer (toolPanelLabel), and setColumnVisible joins the typed PretableReactGrid handle, retiring the documented cast in the column-order tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tool panel's placeholder becomes the real columns section: the full columnLayout roster (hidden entries included and dimmed), subgrouped Pinned left / Columns / Pinned right, with visibility toggles wired to setColumnVisible, case-insensitive label search that hides emptied subgroup labels, and a reset that replays the surface-mount layout (pin, then visibility, then the full-roster order). The grip and kebab complete the row anatomy but stay inert for Tasks 8 and 9. The section subscribes to the engine's layout slice itself via useSyncExternalStore, so the surface's descriptor memo closes over stable handles only — the Task 6 review's stale-closure trap stays fixed, and the memo's handoff comment now says why the deps are safe. Riders from that review: a composition test pinning the group-panel wrapper inside the tool layout's grid area, and the css-cascade guard now asserts overflow:hidden on the wrapper and border-radius:0 on the inner viewport (both mutation-verified). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reset's roster-drift branches — splicing an id added since the surface's capture back at its current position, and filtering out an id removed since — were reachable (the group column joins the layout mid-session) but untested. Two tests now pin them, each proven by mutation: append-instead-of-splice and delete-the-splice both fail the added-column test (the pre-reset order shuffles the initial ids too, because with them already in place the order replay is a no-op and a deleted splice was undetectable), and dropping the removed-ids filter fails the removed-column test (setColumnOrder throws invalid-ui-state on the stale id). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The kebab on each columns-section row now opens a portaled role=menu with Pin left / Pin right / Unpin (current state disabled), reusing the header ColumnMenu's popover attribute contract so grid.css styles it with zero new rules. Escape preventDefaults so the pane's rail-tab focus yank skips it and focus returns to the kebab; selecting refocuses the (remounted) kebab through a per-id node map. Also: an empty-search state line, and the surface's indexedGrid now feeds ColumnsSection with no cast at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An inline (unportaled) menu survived every existing test: jsdom cannot see that the viewport's contain:content would clip a position:fixed popover, so the menu's DOM location is the enforceable proxy. The open test now asserts the menu parents in document.body and NOT in the pane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The menu's anchor rect was frozen at open, so wheel-scrolling the pane's list box left the position:fixed menu drifting — wheel fires neither pointerdown nor a focus change, so nothing closed it. The section now reuses useHeaderPopover (the header popovers fought exactly this): the menu re-measures on capture-phase scroll/resize, follows its kebab, and closes when the anchor is genuinely gone. Searching the open row out now clears the menu STATE at its source (the input's onChange), so clearing the search cannot remount a zombie menu at a stale rect; the render guard now looks up the rendered (roster+search filtered) list and its comment says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pure move ahead of Task 9's drag machinery, per the Task 8 review's file-size note. No behavior change; the 32 tool-panel tests stay green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pointer drag on the grip commits on drop, never mid-move: the pure dropTargetForPointer(y, rowRects, groups) resolves the pointer to an insertion slot + target subgroup (a group gap splits at its middle, so the same slot exists as end-of-group-above and start-of-group-below with different pins), and the drop pins first, then writes the FULL setColumnOrder roster with only the moved id relocated. The commit reads the live target from a ref, not the render closure — Chromium's pointerup can outrun the final move's re-render. Keyboard alternative (a11y gate): the grip is a focusable span (role=button, tabIndex=0 — a native button would drag UA chrome into the [data-pretable-tool-row-grip] CSS) answering Shift+ArrowUp/Down, the group-chip strip's chord turned vertical; crossing a rendered subgroup boundary re-pins via the engine's stable pin regroup. Ends no-wrap no-commit, focus follows the row through fragment remounts. 11 unit tests pin the geometry (jsdom cannot express rects), 9 jsdom tests pin the keyboard path + drag attributes, and the new Playwright spec proves drag, cross-boundary re-pin, the rail/pane tab walk, and the Task 6 minWidth:0 pin on a real page in both engines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t drags cannot drop Three review findings, one systematic-debugging arc: - Capture moves to pointerdown (GroupPanel's rule): engines rAF-coalesce pointermoves, so on a ~14px handle the first DELIVERED move can already be outside the grip and a capture taken in the move handler never happens. The 5px threshold still gates the dragging STATE. Pinned by a jsdom spy on setPointerCapture (jsdom has no real capture, so the spy is the only in-process signal; mutation-checked — late capture fails exactly that test). - The drop now resolves from the pointerup event's OWN coordinates, measured at release: both engines routinely coalesce away the last pointermove before a quick release, so any target tracked during the moves can be one step behind where the user let go — probed as a cross-boundary drop landing on the wrong side of the subgroup split in Chromium and WebKit alike. - Escape mid-drag cancels without engine mutation — the rule both other drag surfaces already follow (header reorder, chip drag); tested. New jsdom coverage: ArrowDown off Pinned-left lands FIRST of the unpinned group; a cross-boundary move under an active search leaves filtered-out ids in place; Escape-cancel; capture timing. The e2e spec's residual flake was the DOCS PAGE, not the component (probed: the site smooth-scrolls every in-page jump, and an intermittent focus-reveal scroll fires ~300ms after the rail click — coordinates measured mid-animation are stale by press time). The spec now declares prefers-reduced-motion (the site's own gate for instant scrolling), waits for layout stability before pressing, verifies drag arming with a bounded re-acquire, and measures travel targets after arming. 60 consecutive e2e tests green on a load-40 box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Playwright 1.62.1 has no top-level reducedMotion test option: next build's typecheck fails on the bare key (TS2353, which would red-X the required website build gate) and at runtime the unknown key is silently ignored — the previous green sweep was carried by the other hardenings with smooth scroll still active. contextOptions reaches browser.newContext, so the emulation actually applies now. Also trims an overclaiming comment: the Escape-cancel's preventDefault only interlocks the pane's handler while focus sits outside the pane (document bubble listeners run after the React-root handler) — the chip drag's cancel shares the characteristic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The consumer-facing half of Tool Panel SP1. The <Pretable> preset gains
the toolPanel passthrough (default-on already reached it — the default
lives in the surface — so the new prop is the ability to disable or
configure), with a test proving the rail renders by default and
toolPanel={false} removes it. react.api.md regenerated for the new
PretableBaseProps member.
New grid/tool-panel.mdx documents the default-on stance, the
PretableToolPanelConfig contract, the columns section's operations, and
the verified keyboard reference, led by a live example whose
defaultActiveSection opens the Columns pane on load. The docs guard's
rosters register the page's Configuration table (bound complete to
PretableToolPanelConfig, with Type and Required columns checked) and
the ToolPanelSectionId enumeration. pretable-component.mdx's
Limitations paragraph now tells the truth about the one piece of
config UI that ships on by default, pretable-surface.mdx's
Configuration table gains the Tool panel row, and the sidebar lists
the page after Column layout.
Rider from Task 9's review: the e2e keyboard-chord test now asserts
the initial header order before the press, so roster drift cannot make
its post-press assertion pass vacuously.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… test comment Three accuracy fixes from spec review. The keyboard table's Escape row now states the layered-dismissal reality (mid-drag cancel and menu close dismiss first; only with nothing open does focus return to the rail tab); the controlled-state hazard says re-imposition happens when the write-back effect re-runs rather than promising the very next render; and the preset test's comment no longer claims a named-prop forward the implementation (one spread object, gated by the interface member) does not perform. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
Vercel preview readyPreview: https://pretable-dy36w9t3e-cacheplane.vercel.app Updated automatically by the |
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.
What this is
The tool panel, SP1 of three: a vertical icon rail docked at the grid's right edge, opening a full-height pane — shipped on by default on
PretableSurfaceand the<Pretable>preset, with its first section, Columns: visibility toggles, search, reset, a per-row pin menu, and drag + keyboard reorder. SP2 (the filter builder over the typed operator model) and SP3 (grouping config) slot into the same section contract.Spec:
docs/superpowers/specs/2026-08-24-tool-panel-design.md. Plan:docs/superpowers/plans/2026-08-24-tool-panel-sp1-shell-columns.md.The one engine change: column visibility
It didn't exist.
hidden?: booleannow lives oncolumnLayoutentries — hidden columns keep their position, width, and pin state (re-showing restores them) and are filtered out at the drawn-order projections, so the seven span-resolving consumers (copy, paste, selection, announcements…) inherit correctness from the single source of truth they already read.setColumnVisiblerepairs focus, selection anchor, and open edit sessions to the nearest visible neighbor; keyboard navigation skips hidden columns;beginEditrefuses them. The public column-order vocabulary is uniformly drawn:onColumnOrderChange,state.columnOrder,moveColumn,getColumns()— hidden ids exist only in the engine layout, spliced back in at the two write paths (each with its own documented semantic, both test-pinned against unification).Design notes
tablist— roving tabindex, one Tab stop, arrows move focus without activating, Escape in the pane returns to the tab. The WCAG tab-exit guard (grid-tab-wrap-rows.spec.ts) learned the rail as a named, asserted stop: one press past it must land outside the grid, so a second stop or a trap fails loudly.useHeaderPopover, so it follows its row through scroll and closes when the anchor is gone. The card chrome (border/radius/shadow) moved from the viewport to the new layout wrapper so the rail sits inside the card — comment-guarded in both directions, pixel-proven under all four theme states.pointerupevent's own coordinates and capture is taken atpointerdown— both fixes came out of real cross-engine races the e2e reproduced (pointerup outrunning the last coalesced pointermove; the first delivered move landing outside a 16px grip).Shift+ArrowUp/Downon the focused grip (the chip strip's chord, verticalized); crossing a subgroup boundary is pin-only, and the engine's stable partition provably lands the row at the near edge.Review process
Every task ran implementer → spec review → quality review, with mutation testing throughout (~40 mutations across guards, geometry, capture timing, and a11y). Defects caught and fixed before merge include: unpinning a hidden column revealing it,
setColumnsdropping visibility-only updates, a selected-tab hover clobber via thebackgroundshorthand, the pin menu drifting on pane scroll, the two pointer races above, andreducedMotionsilently ignored as a top-level Playwright option (which would also have red-X'd the build gate).Verification
--pretable-bg-headerin light/dark/excel/material; card chrome on the wrapper, none doubled on the viewport; pane at 264pxChangesets:
@pretable/coreminor,@pretable/react+@pretable/uiminor. Rebased onto2ad7feea(#485's copy/paste coupling test passes against the drawn-order change).Known follow-ups (deliberate, not oversights)
role="menu", andusePendingFocusat the third pending-focus trio.🤖 Generated with Claude Code