Design Button Behavioral Stability Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Make native Button intent explicit, test its behavioral contract, and block design-system changes from merging when they break any direct monorepo consumer.Architecture: Keep the runtime-safe type="button" fallback in the shared component while making direct JSX usage mandatory through a focused ESLint rule. The Button type is lint-enforced, with a defensive runtime default. Audit the dependency graph from package metadata, migrate each direct Button use according to its form/event context, add DOM-level component and dashboard regression tests, and extend the existing design-system workflow with metadata-derived downstream checks that use each package’s canonical build surface.Tech Stack: React 19, TypeScript, ESLint 9 RuleTester, Vitest/jsdom, pnpm, Nix/exapkgs, GitHub Actions.
Add the existing repository versions of Vitest/jsdom and React DOM test helpers to the design package, plus a test:button/unit-test script that runs DOM tests without weakening the existing Node test suite.
Write Button behavioral tests that assert the rendered DOM type, submit activation by click and keyboard, non-submission for type="button", and no click/submit action while disabled or loading.
Extract only the shared team-settings dialog form markup into AddableTableDialogForm so it can be exercised without mocking the entire 1,600-line page; keep styling, request handlers, and the merged type="submit" hotfix unchanged.
Write dashboard DOM tests that fill and click the visible submit control for both “Send invite” and “Add IP” configurations and assert the supplied submit handler fires; add pending/disabled coverage.
Run the new focused tests once against the pre-contract code and confirm they encode actual interaction rather than source-string presence.
Keep ButtonProps.type?: "button" | "submit" | "reset" and retain type = "button" inside the runtime implementation for JavaScript/untyped callers; enforce explicit source intent with the focused ESLint rule.
Add @exa/design/require-button-type, tracking named imports and aliases from @exa/design/ui/button and reporting direct JSX uses with no explicit type attribute.
Document the deterministic policy: a spread or dynamic type does not satisfy the direct-JSX lint rule because intent must be visible at the use site; wrappers/re-exports must preserve native type intent at their forwarding boundaries.
Add RuleTester cases for all three valid literal types, aliases, unrelated local Button components, missing type, spread-only props, and explicit dynamic type.
Enable the rule in the shared preset and keep the docs/rule catalog drift test green.
Run node --test src/eslint/rules.test.js src/eslint/consumer-rules-doc.test.js and the focused Button test.
Modify: direct Button consumers under every package discovered from package.json metadata, including go/cortex/ui, infra/*, node/apps/*, and typescript/*
Modify: Button examples and internal consumers under node/apps/design/src as required by the new type contract
Generate an audit table from the TypeScript AST that records package, file, imported local name, wrapper/re-export status, enclosing form (where statically visible), handler, disabled/loading state, and chosen button|submit|reset intent.
Review form ancestry and handler behavior before editing: use submit only for controls intended to invoke a form’s onSubmit, reset only for actual resets, and button for triggers/actions (especially dialogs/popovers nested in forms).
Apply the reviewed mechanical edits, including aliased imports, wrappers, styled wrappers, docs examples, and tests; do not change visual props or product behavior.
Re-run an AST inventory and TypeScript checks to prove no typed Button or wrapper callsite remains ambiguous.
Record the total changed callsites and the reviewed submit/reset list for the final handoff.
Export a semantic-safety config (or rule block) containing require-button-type independently from visual migration rules, without relaxing the existing recommended preset.
Update dashboard so require-button-type applies to all application TS/JS while MIGRATED_PATHS continues to scope visual/component compliance.
Apply the same app-wide safety rule to every direct consumer with ESLint; where a consumer uses oxlint or has no ESLint, rely on the TypeScript gate and record that limitation explicitly.
Add a metadata-derived checker that discovers every direct @exa/design package and verifies its source is covered either by the ESLint safety rule or by a TypeScript compatibility check.
Test discovery against fixtures plus the live monorepo graph so newly added consumers cannot silently skip the gate.
Modify: node/apps/design/project.nix only if a reusable Nix passthru is needed
Modify: relevant canonical consumer project.nix files only when their existing output does not expose the package’s build/typecheck/test target
Implement consumer discovery by scanning tracked application package.json files for dependencies/devDependencies/peerDependencies on @exa/design; emit stable JSON with package name, directory, nearest canonical Nix project/flake, and available typecheck, build, lint, and behavioral-test scripts.
Add unit tests for workspace, file:, nested-package, missing-script, and newly discovered consumer cases.
Extend the design-system PR workflow with a discovery job and a blocking matrix. Always run design lint/typecheck/unit tests plus every consumer typecheck and relevant contract test; run production builds through each consumer’s canonical Nix/build definition where available.
Avoid a handwritten package list: the matrix must come from the discovery script, and an unclassifiable consumer must fail discovery with an actionable error rather than being skipped.
Add a scheduled full consumer matrix only if the blocking PR build set must be reduced for measured cost; preserve at least one blocking downstream check capable of detecting compile/API incompatibility.
Validate workflow syntax and dry-run/inspect the emitted matrix locally.
Task 6: Document behavioral compatibility as public API
Files:
Modify: node/apps/design/AGENTS.md
Modify: node/apps/design/public/AGENTS.md
Modify: node/apps/design/public/CHECKLIST.md
Modify: node/apps/design/DECISIONS.md
Modify: node/apps/design/docs/eslint.md
Modify: node/apps/dashboard/AGENTS.md
Add a new durable decision stating that DOM element choice, form semantics, callbacks/payloads, controlled state, focus, dismissal, keyboard interaction, and disabled/loading behavior are public API and require downstream interaction tests.
Require an explicit Button type in contributor and consumer guidance; state why the runtime default remains button and why visual parity is insufficient.
Modify: only formatter output in files already changed
Run format --branch origin/master per the monorepo code-quality contract and inspect the resulting diff for unrelated rewrites.
From node/apps/design, run lint, typecheck, unit tests, the ESLint RuleTester suite, the Button DOM tests, and production build.
From node/apps/dashboard, run format/check, lint (including app-wide semantic safety), typecheck, the shared team-settings regression tests, relevant unit tests, and production build with documented fake environment variables.
Execute the compatibility checker/matrix locally for all metadata-derived direct consumers, using canonical Nix outputs where available; capture exact command/failure/reason for any environment-blocked package.
Run git diff --check, recount changed Button callsites, and compare the final consumer inventory with the initial package/import scans.
Review git diff origin/master... for UI redesign, accidental runtime-default changes, broad lint suppressions, stale generated artifacts, or changes outside the requested failure class.
Last modified on August 27, 2026
Assistant
Responses are generated using AI and may contain mistakes.