feat(react): PretableButton, PretableIconButton, and the components slot - #580
Merged
Conversation
Opens the component-kit program: every control in the grid rebuilt on a pre-built, accessible, styleable kit a consumer can also replace per component type. Four sub-projects by component type; this spec is the first and settles the contract the rest inherit — placement in @pretable/react, one `components` slot per type with a `role` in the props, the house styling channel (attributes + tokens + className passthrough), and context as the plumbing because popovers are portalled. Proven against the grid's twelve plain push-buttons, which keep their existing data attributes so nothing that identifies a button today stops identifying it. Menu items, checkboxes, tabs and the twisty are roles that happen to use <button>, and wait for their own components. Also records the decision that the Excel theme stays light-only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two corrections from reading the code the plan has to match. The four labelled actions are `border: 0; background: transparent` with a hover tint — a ghost button, not an outlined one; the earlier survey read `border: 0` as a border. And `role` cannot be the prop: it is the ARIA attribute on every button, so a replacement spreading `role="filter-clear"` onto a <button> would emit invalid ARIA. The prop is `site`, which is what the spec already defined it to mean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fifteen tasks, TDD throughout: the pixel baseline first, the two components, the type contract, the context, the surface wiring with the override tests landing red, the component rules and a site-rule guard that stays red until the twelve sites migrate and their CSS collapses, the public API, the pixel re-measure, docs with registered tables, the browser proof through the portalled dialog, and a verification step that runs the bench suite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kit's first two components. Always type="button"; className and style pass through; the contract attributes follow the spread so no prop displaces them; IconButton's accessible name is a required prop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tract attributes "aria-label": string makes omission a compile error but lets "" through, which is the failure the prop exists to prevent; warnOnce closes it in development the way PretableStatus's missing-children warning does. The hostile-spread test now pins all four contract attributes, and `site` is asserted never to reach the DOM. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The type requires it; the runtime cannot, so the guard that warns about an empty name must not itself throw on an absent one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
aria-label required on IconButton, no `type` prop, an open `site`, a closed variant, and native attributes flowing through with the ref. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One slot per type, resolved over the defaults and memoised on each slot's identity; context rather than props because the popovers are portalled. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ostile cast `Extract<PretableButtonSite, "filter-clear">` was the same whether or not the union was open, so the alias could not fail for the reason its name claimed; `Equal` against the full union can, and was mutation-checked. The source-relative import is marked as the temporary state it is, and the missing-name test casts to the component's own props type. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An empty dependency array passed every test, because every test fed the memo unchanged values. The negative twin now fails it. Also: the React 18 forwardRef obligation is stated where the type cannot enforce it, both branches of the resolution are frozen, and the context has a DevTools name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolved once, memoised on slot identity, provided on both of the surface's return paths so the portalled popovers inherit it. The override tests land red on the site assertions, which the migrations turn green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shared push-button box on the component attributes, first in the file so site rules still win by order; ghost and link looks; the standard ring, disabled treatment and forced-colours ink. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Red until the site rules collapse onto the component rules (Task 10). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
+ filter, + group, Add group, Expand all, Collapse all. Each keeps its site attribute, so nothing that identified it stops identifying it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…variant The Clear button is inside the portalled filter dialog, which is the case the components context exists for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…est says what it pins The kebab-menu test opened the menu on the built-in as readily as on a replacement, so it could not fail; it now asserts the node is the replacement first. The remount test's comment claimed the memo, which a same-type reconcile hides from it; the memo is pinned at the hook level, and the comment now points there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The site-rule guard demanded zero :focus-visible rules per site, which would have forced the funnel's and the column menu's keyboard reveal (opacity: 1) out of the file — a focus-visibility regression arriving through a test whose message said the opposite. It now forbids the ring's DECLARATION at a site, keeps a positive check of what each site genuinely owns so a deleted rule cannot pass, forbids the whole shared box rather than five of its lines, and its list is mirrored into the kit guard so the two read the same list from both ends. A third guard pins the source order the design rests on. Two stay red by design until the site rules collapse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Funnel, column menu, row kebab, chip remove, filter-row remove and group remove. Every site keeps its attribute, its ref, its tabindex and its handlers; the override suite goes fully green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Twelve sites keep a size, a reveal, an alignment; the box, ring and disabled
ink are the component's. Kit state rules move to the end of the layer so a
site's own state colour cannot outrank the disabled ink; position: relative
leaves the kit icon rule and stays only where a ::after hit area needs it
(funnel, column menu, row kebab) — on the two remove buttons, which sit in
flow among later siblings, it would have reordered painting; the two shared
focus lists are split, not deleted, so the five non-button controls on them
keep their ring.
Deliberate changes, from the review, not regressions: filter-clear and
tool-reset gain the kit's border-radius and the product focus ring
(tool-reset wore the UA ring); for a consumer without a CSS reset
filter-clear also gains the dialog's font, which Tailwind preflight already
gave it on our own site; the funnel and the column-menu button gain
flex: none.
Four older guards read declarations that have moved onto the kit rules
(a radius, two padding: 0s, box-sizing, the ghost box and its disabled
state, and the existence of a [data-pretable-filter-add] rule). Each is
re-pointed at the kit rule that now carries it rather than dropped, so the
same claim is still made from the one place it is true.
Guards mutation-checked: border: 0 added back to the funnel's base rule
fails "a push-button site rule declares only what is its own" ("redeclares
/border:\s*0/ — the kit button rule owns it"); the kit :disabled rule moved
back to the top section fails "the kit rules bracket the site rules in
source order" ("expected 2545 to be greater than 36192"). Both reverted;
packages/ui 109/109, packages/react override suite 7/7, typecheck and lint
clean.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ew minors All twelve sites are now asserted in jsdom to carry the kit attribute and their site name — the bridge that makes the CSS guards load-bearing, since a site quietly back on a raw <button> would have left them green. Also: JSDoc on the two components slots (their report markers were "undocumented"), a key-omissibility pin in the type test, the components hook read at the top of every component body, and a stale "Task 8/9" docblock in ColumnsSection removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…disabled The ✕ is a CloseIcon sized by --pretable-icon-size, so font-size and line-height sized nothing and the comment that justified them was untrue. Every icon site's hover now carries :not(:disabled), and the site guard keeps it — latent today, since no icon site is disabled, but the rule is #573's. The bracket guard's comment no longer describes a red state that has since gone green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s per comma-part Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A replacement lands inside the portalled filter dialog, and the grid still anchors its menu on and returns focus to a replaced icon button. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An icon button carries no variant; the site attribute is written only when a site is given; the example replaces labelled buttons only, and its Clear lives in a column funnel's dialog, not the Filters section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e-height font: inherit on the kit button pulled the host page's line-height into the filter dialog's Clear and every menu item; the surfaces declared family, size and colour but not the fourth member. 1.5 is what the grid's own body computes and what the pixel baseline measured. The portaled-trio guard now requires it on these two blocks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Task 13 moved the three unit-test count constants and missed this e2e one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
blove
enabled auto-merge (squash)
September 5, 2026 19:14
Contributor
Vercel preview readyPreview: https://pretable-uq0i1kh3v-cacheplane.vercel.app Updated automatically by the |
Merged
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.
Components SP1 — the contract, proven on the grid's twelve plain push-buttons. Spec:
docs/superpowers/specs/2026-09-04-components-sp1-button-design.md; plan:docs/superpowers/plans/2026-09-04-components-sp1-button.md.What ships
PretableButton(ghost|link) andPretableIconButton(aria-labelrequired — a compile error, not a WCAG failure found later; an empty one warns in development). Alwaystype="button";className/stylepass straight through;refforwarded; styled throughdata-pretable-button/-icon-button/-variant/-site.components={{ Button, IconButton }}on the surface and the preset: one slot per type, applied everywhere, resolved once and memoised on slot identity, delivered by context so the portalled filter dialog gets it too. The placement prop issite, notrole—roleis the ARIA attribute on every button.grid.cssis net smaller.Out of scope by decision (spec): menu items, checkboxes, the tab, the twisty, header cells,
filter-join, and the editors' internal month-step / number-stepper buttons — each waits for its own component.Verification
position: relativeoff the kit icon rule and kept per-site where a::afterhit area needs it).:focus-visiblerules per site, which would have deleted the funnel's and column menu's keyboard reveal; it was rewritten to check declarations, not selector counts, and to require what each site genuinely owns so a deleted rule cannot pass. Kit base rules first, state rules last; every site hover carries:not(:disabled), per comma-part. The two portalled surfaces that host kit controls now declareline-heighttoo —font: inherithad been pulling the host page's intoClearfor a consumer with no CSS reset.dist/.aria-labelrequired,typerejected,siteopen and keeping built-in autocomplete (the first assertion was mutation-proved not to test openness), slots optional and omissible./fixtures/components). Every labelled and icon button is the consumer's; the dialog's Clear is a child of<body>and is the replacement; a replaced kebab still anchors its menu at the placement rule's exact clamp, and Escape returns focus through the forwarded ref (the assertion that actually depends on it — the menu anchor comes fromcurrentTargeteither way)."aria-label"), so the page's most load-bearing prop was invisible to it until both readers were widened (blast radius: one member in the whole corpus).format,lint,typecheck,typecheck:public,api:check,test,bench:e2e(29), and the website's smoke / tool-panel / header-keyboard / popover-scroll / grouping / components suites (146 + 38) pass. One website unit test (partial-row-streamdemo) timed out twice under a load average of 18–20 and passed 3/3 in isolation; it touches nothing on this branch.🤖 Generated with Claude Code