Skip to main content

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.

Task 1: Freeze the incident as failing contracts

Files:
  • Modify: node/apps/design/package.json
  • Modify: node/apps/design/pnpm-lock.yaml
  • Create: node/apps/design/src/ui/button.test.tsx
  • Create: node/apps/dashboard/src/app/(app)/(dashboard)/team-settings/AddableTableDialogForm.tsx
  • Create: node/apps/dashboard/src/app/(app)/(dashboard)/team-settings/AddableTableDialogForm.vitest.tsx
  • Modify: node/apps/dashboard/src/app/(app)/(dashboard)/team-settings/TeamSettingsPageClient.tsx
  • Modify: node/apps/dashboard/package.json
  • Modify: node/pnpm-lock.yaml
  • 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.

Task 2: Require Button intent in TypeScript and ESLint

Files:
  • Modify: node/apps/design/src/ui/button.tsx
  • Modify: node/apps/design/src/eslint/rules.js
  • Modify: node/apps/design/src/eslint/rules.test.js
  • Modify: node/apps/design/src/eslint/index.js
  • Modify: node/apps/design/src/docs/consumer-lint-rules.json
  • Modify: node/apps/design/docs/eslint.md
  • 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.

Task 3: Audit and migrate every Button callsite

Files:
  • 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
  • Create: .context/design-button-stability/button-audit.tsv (gitignored audit evidence)
  • 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.

Task 4: Make semantic safety lint app-wide

Files:
  • Modify: node/apps/dashboard/eslint.config.mjs
  • Modify: direct-consumer ESLint configs where they scope the visual preset or compose only selected rules
  • Create: node/apps/design/scripts/check-consumer-button-safety.mjs
  • Test: node/apps/design/scripts/check-consumer-button-safety.test.mjs
  • 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.

Task 5: Add downstream compatibility CI

Files:
  • Modify: .github/workflows/design-system.yaml
  • Create: node/apps/design/scripts/list-consumers.mjs
  • Create: node/apps/design/scripts/list-consumers.test.mjs
  • Modify: node/apps/design/package.json
  • 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.
  • Add the migration/review checklist covering rendered HTML, forms, link-vs-action, keyboard/focus, controlled state, callbacks, disabled/loading, popup dismissal, analytics/requests, and behavioral regression tests.
  • State that safety-oriented lint is app-wide and independent of gradual visual migration coverage.
  • Update dashboard’s shared-component incident guidance with this form-submission regression and the new prevention gates.

Task 7: Format and validate the complete change

Files:
  • 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