diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d159fd..e032a01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,52 @@ ## Unreleased +The vendored `@aestheticfunction/dspack-gen/core` bundle moves from the +v0.1.2 release tag (`c5bfd6d`) to the v0.5.0 release tag (`5203569`), the +current latest. **This changes what `validate-ui` rejects** — it is a +behavior change to a published tool, not a maintenance bump: + +- Gate S2 now enforces sub-component **containment** (spec v0.4 §5.1): a + declared sub-component is valid only within its declaring compound's + subtree, so a `dialog-title` with no `dialog` ancestor is now a + vocabulary error. Previously accepted. (dspack-gen 0.3.0) +- Gate S3 now evaluates **`requiredCategories`** on `required-composition` + rules (spec v0.4 §4.3), so a contract can require that a matching node + carry a descendant drawn from a named category. (dspack-gen 0.2.2) +- `get-generation-context` serves a richer system prompt: each component's + vocabulary line now carries that component's `composition.notes` from the + contract, capped at two sentences. Prompt material only — no S-gate + changes with it. (dspack-gen 0.5.0) +- Unchanged: `forbiddenCategories` was already evaluated at v0.1.2, and the + lookup tools and gate S1 are unaffected. Exactly four shipped `src/core` + files differ across the whole v0.1.2 -> v0.5.0 move: `lint/vocabulary.ts` + (S2 containment), `contract.ts` + `lint/rules.ts` (S3 requiredCategories), + and `compiler.ts` (the prompt change). dspack-gen 0.4.0/0.5.0's other work + — repair loop, casualty-free generation view, join-id view — lives in + `run/`, which ds-mcp does not vendor. + +Coupled updates that had to land in the same change: + +- `examples/shadcn-ui-v04.dspack.json` re-synced from the spec repo + (contract 2.3.0 -> 3.2.0: 8 -> 34 components, 8 -> 49 rules). It exercises + both new evaluations, and it is the file the quick start downloads. +- `src/schema/dspack.v0.4.schema.json` re-synced from the spec repo, which + had added `requiredCategories` to `required-composition` (§4.3). Without + it the loader **rejected the spec's own example contract**. All four + schemas are now tracked in `scripts/check-sync.mjs`; v0.1-v0.3 were + already byte-exact. +- The golden-context test now reads dspack-gen's contract fixture + (`src/tests/fixtures/shadcn.v0_4.contract.json`, newly drift-tracked) + instead of the shipped example. The golden is compiled from that contract, + and the two upstreams are no longer byte-equal — the spec repo moved to + 3.2.0 while dspack-gen's fixture stayed at 2.3.0. The test now compares + compiler output against the compiler's own input, which is what it was + always meant to pin. +- `rule.button-carries-text` (new in contract 3.2.0) requires a button's + own text, so the `textScope: subtree` fixture in + `generation-tools.test.ts` moves its label onto the button; the trigger + still carries no text of its own, which is what that test pins. + - The v0.1 example `examples/shadcn-ui.dspack.json` is renamed to `examples/shadcn-ui-v01.dspack.json` (DX-1, D4): the old unversioned filename collided with the dspack repository's v0.4 diff --git a/README.md b/README.md index 9dc5ffe..a10a886 100644 --- a/README.md +++ b/README.md @@ -164,16 +164,41 @@ the guarantee. dspack-gen is a **build-time** dependency: its `core` subpath is bundled into the published package (`dist/vendor/dspack-gen-core.js`) from a commit-pinned devDependency, so installing ds-mcp from npm pulls nothing -from git. The default posture is **tag-pinned**: the pin is the commit of -dspack-gen's latest release tag, so the vendored core corresponds exactly -to a published dspack-gen version. CI watches this two ways: -`scripts/check-core-pin.mjs` fails loudly when a newer dspack-gen release -changes shipped `src/core` files (test files are excluded — the documented -escape for intentional ahead-of-release pins), and the golden-context test -byte-compares `get-generation-context` output against dspack-gen's own -compiler golden. Updating governance semantics means re-pinning to the new -release tag commit, `npm install`, `npm run build`, verifying the golden -tests, and republishing ds-mcp. +from git. The posture is **tag-pinned**: the pin is always the commit of a +dspack-gen *release tag*, so the vendored core corresponds exactly to a +published dspack-gen version. + +**Currently vendored: dspack-gen `v0.5.0`** (`5203569`), dspack-gen's +latest release. Moving here from v0.1.2 widened what `validate-ui` rejects, +in exactly two places. Gate S2 now enforces sub-component **containment** +(spec v0.4 §5.1): a declared sub-component is valid only inside its +declaring compound's subtree, so a `dialog-title` with no `dialog` ancestor +is a vocabulary error rather than an accepted node. Gate S3 now evaluates +**`requiredCategories`** on `required-composition` rules (§4.3), letting a +contract require that a matching node have a descendant drawn from a named +category. Surfaces that passed under the older core can fail under this one +— that is the point of the move, and it is why re-pinning is a deliberate +release step rather than an automatic follow. `forbiddenCategories` was +already evaluated at v0.1.2 and is unchanged, as are the lookup tools. + +`get-generation-context` also serves a richer system prompt: from +dspack-gen 0.5.0 each component's vocabulary line carries that component's +`composition.notes` from the contract (capped at two sentences). Nothing +about the S-gates changes with it — it is prompt material, and prompt +steering is not enforcement. + +CI watches the vendoring three ways. `scripts/check-core-pin.mjs` fails +loudly when a newer dspack-gen release changes shipped `src/core` files +(test files are excluded — the documented escape for intentional +ahead-of-release pins). `scripts/check-sync.mjs` byte-compares every copied +artifact against its upstream, including the four dspack JSON Schemas the +loader validates against — a stale schema copy is not cosmetic, it makes +ds-mcp refuse contracts the spec considers valid. And the golden-context +test byte-compares `get-generation-context` output against dspack-gen's own +compiler golden, read against the contract dspack-gen compiled that golden +from. Re-pinning means moving the devDependency to the new release tag +commit, `npm install`, `npm run build`, `node scripts/check-sync.mjs +--write`, verifying the golden tests, and republishing ds-mcp. ## Requirements diff --git a/examples/shadcn-ui-v04.dspack.json b/examples/shadcn-ui-v04.dspack.json index 6ebd8d7..1163a5f 100644 --- a/examples/shadcn-ui-v04.dspack.json +++ b/examples/shadcn-ui-v04.dspack.json @@ -3,7 +3,7 @@ "dspack": "0.4", "name": "shadcn/ui", "description": "A collection of reusable components built with Radix UI and Tailwind CSS. Components are copied into your project, not installed as a dependency.", - "version": "2.3.0", + "version": "3.2.0", "metadata": { "source": "https://ui.shadcn.com", "license": "MIT" @@ -171,6 +171,16 @@ "category": "color-primitives", "token": "slate-950" } + }, + "popover": { + "value": "hsl(0, 0%, 100%)", + "type": "color", + "description": "Surface color for floating panels (Popover, DropdownMenu, Tooltip content)." + }, + "popover-foreground": { + "value": "hsl(222.2, 84%, 4.9%)", + "type": "color", + "description": "Text color on floating panel surfaces." } } }, @@ -237,12 +247,30 @@ "type": "enum", "propRole": "choice", "values": [ - { "value": "default", "description": "Standard button for primary page actions." }, - { "value": "destructive", "description": "For irreversible actions like delete. Uses destructive color tokens." }, - { "value": "outline", "description": "Bordered button for secondary actions that need clear affordance." }, - { "value": "secondary", "description": "De-emphasized button for supplementary actions." }, - { "value": "ghost", "description": "Minimal button for toolbar actions or inline controls. No background until hovered." }, - { "value": "link", "description": "Renders as a text link. Use for inline navigation-like actions within prose." } + { + "value": "default", + "description": "Standard button for primary page actions." + }, + { + "value": "destructive", + "description": "For irreversible actions like delete. Uses destructive color tokens." + }, + { + "value": "outline", + "description": "Bordered button for secondary actions that need clear affordance." + }, + { + "value": "secondary", + "description": "De-emphasized button for supplementary actions." + }, + { + "value": "ghost", + "description": "Minimal button for toolbar actions or inline controls. No background until hovered." + }, + { + "value": "link", + "description": "Renders as a text link. Use for inline navigation-like actions within prose." + } ], "default": "default", "description": "Visual treatment. Use destructive for irreversible actions, ghost for toolbar actions, link for inline text actions." @@ -251,10 +279,22 @@ "type": "enum", "propRole": "dimension", "values": [ - { "value": "default", "description": "Standard size, suitable for most contexts." }, - { "value": "sm", "description": "Compact size for dense UIs or inline actions." }, - { "value": "lg", "description": "Large size for prominent calls to action." }, - { "value": "icon", "description": "Square aspect ratio for icon-only buttons." } + { + "value": "default", + "description": "Standard size, suitable for most contexts." + }, + { + "value": "sm", + "description": "Compact size for dense UIs or inline actions." + }, + { + "value": "lg", + "description": "Large size for prominent calls to action." + }, + { + "value": "icon", + "description": "Square aspect ratio for icon-only buttons." + } ], "default": "default", "description": "Button size. Use icon for square icon-only buttons." @@ -275,8 +315,14 @@ "accessibility": { "role": "button", "keyboardInteractions": [ - { "key": "Enter", "description": "Activates the button." }, - { "key": "Space", "description": "Activates the button." } + { + "key": "Enter", + "description": "Activates the button." + }, + { + "key": "Space", + "description": "Activates the button." + } ], "labelRequirement": "required-accessible-name", "requiredAttributes": [ @@ -309,10 +355,28 @@ "severity": "must" } ], - "tokens": ["primary", "primary-foreground", "secondary", "secondary-foreground", "destructive", "destructive-foreground", "border", "ring", "radius"], - "relatedComponents": ["toggle"], - "tags": ["interactive", "form", "action"], - "categories": ["interactive"] + "tokens": [ + "primary", + "primary-foreground", + "secondary", + "secondary-foreground", + "destructive", + "destructive-foreground", + "border", + "ring", + "radius" + ], + "relatedComponents": [ + "toggle" + ], + "tags": [ + "interactive", + "form", + "action" + ], + "categories": [ + "interactive" + ] }, "alert-dialog": { "name": "Alert Dialog", @@ -345,10 +409,22 @@ } ], "keyboardInteractions": [ - { "key": "Tab", "description": "Moves focus between focusable elements within the dialog. Focus is trapped inside." }, - { "key": "Escape", "description": "Does NOT close the dialog. User must select an explicit action." }, - { "key": "Enter", "description": "Activates the focused button." }, - { "key": "Space", "description": "Activates the focused button." } + { + "key": "Tab", + "description": "Moves focus between focusable elements within the dialog. Focus is trapped inside." + }, + { + "key": "Escape", + "description": "Does NOT close the dialog. User must select an explicit action." + }, + { + "key": "Enter", + "description": "Activates the focused button." + }, + { + "key": "Space", + "description": "Activates the focused button." + } ], "focusManagement": "On open, focus moves to the cancel button (least destructive action). On close, focus returns to the trigger element. Focus is trapped within the dialog while open.", "labelRequirement": "required-visible" @@ -362,7 +438,9 @@ "required": true, "slot": "trigger", "acceptsChildren": "components", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "alert-dialog-content", @@ -406,7 +484,9 @@ "description": "The button that confirms the destructive action.", "required": true, "acceptsChildren": "text", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "alert-dialog-cancel", @@ -414,7 +494,9 @@ "description": "The button that cancels and closes the dialog.", "required": true, "acceptsChildren": "text", - "categories": ["interactive"] + "categories": [ + "interactive" + ] } ], "notes": "AlertDialogContent must contain AlertDialogTitle and AlertDialogDescription for accessibility. AlertDialogAction and AlertDialogCancel must appear in AlertDialogFooter. Place cancel before confirm in reading order." @@ -436,10 +518,29 @@ "severity": "must-not" } ], - "tokens": ["background", "foreground", "primary", "primary-foreground", "destructive", "destructive-foreground", "border", "radius"], - "relatedComponents": ["dialog", "button"], - "tags": ["modal", "confirmation", "destructive", "accessibility"], - "categories": ["overlay"] + "tokens": [ + "background", + "foreground", + "primary", + "primary-foreground", + "destructive", + "destructive-foreground", + "border", + "radius" + ], + "relatedComponents": [ + "dialog", + "button" + ], + "tags": [ + "modal", + "confirmation", + "destructive", + "accessibility" + ], + "categories": [ + "overlay" + ] }, "dialog": { "name": "Dialog", @@ -474,9 +575,18 @@ } ], "keyboardInteractions": [ - { "key": "Tab", "description": "Moves focus between focusable elements within the dialog. Focus is trapped inside." }, - { "key": "Escape", "description": "Closes the dialog and returns focus to the trigger." }, - { "key": "Enter", "description": "Activates the focused interactive element." } + { + "key": "Tab", + "description": "Moves focus between focusable elements within the dialog. Focus is trapped inside." + }, + { + "key": "Escape", + "description": "Closes the dialog and returns focus to the trigger." + }, + { + "key": "Enter", + "description": "Activates the focused interactive element." + } ], "focusManagement": "On open, focus moves to the first focusable element inside the dialog. On close, focus returns to the trigger element. Focus is trapped within the dialog while open.", "labelRequirement": "required-visible" @@ -489,7 +599,9 @@ "description": "The element that opens the dialog.", "slot": "trigger", "acceptsChildren": "components", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "dialog-content", @@ -531,7 +643,9 @@ "name": "DialogClose", "description": "A button that closes the dialog.", "acceptsChildren": "text", - "categories": ["interactive"] + "categories": [ + "interactive" + ] } ] }, @@ -547,10 +661,23 @@ "severity": "must-not" } ], - "tokens": ["background", "foreground", "border", "radius"], - "relatedComponents": ["alert-dialog"], - "tags": ["modal", "overlay", "form"], - "categories": ["overlay"] + "tokens": [ + "background", + "foreground", + "border", + "radius" + ], + "relatedComponents": [ + "alert-dialog" + ], + "tags": [ + "modal", + "overlay", + "form" + ], + "categories": [ + "overlay" + ] }, "card": { "name": "Card", @@ -614,9 +741,22 @@ "severity": "should-not" } ], - "tokens": ["background", "foreground", "border", "radius", "muted", "muted-foreground"], - "relatedComponents": ["button"], - "tags": ["layout", "container", "surface"] + "tokens": [ + "background", + "foreground", + "border", + "radius", + "muted", + "muted-foreground" + ], + "relatedComponents": [ + "button" + ], + "tags": [ + "layout", + "container", + "surface" + ] }, "input": { "name": "Input", @@ -665,10 +805,22 @@ "severity": "should-not" } ], - "tokens": ["background", "foreground", "border", "ring", "radius-sm"], + "tokens": [ + "background", + "foreground", + "border", + "ring", + "radius-sm" + ], "relatedComponents": [], - "tags": ["form", "input", "text"], - "categories": ["interactive"] + "tags": [ + "form", + "input", + "text" + ], + "categories": [ + "interactive" + ] }, "badge": { "name": "Badge", @@ -681,10 +833,22 @@ "type": "enum", "propRole": "choice", "values": [ - { "value": "default", "description": "Standard badge for general categorization." }, - { "value": "secondary", "description": "De-emphasized badge for supplementary metadata." }, - { "value": "outline", "description": "Bordered badge for subtle labeling." }, - { "value": "destructive", "description": "Indicates errors or critical status." } + { + "value": "default", + "description": "Standard badge for general categorization." + }, + { + "value": "secondary", + "description": "De-emphasized badge for supplementary metadata." + }, + { + "value": "outline", + "description": "Bordered badge for subtle labeling." + }, + { + "value": "destructive", + "description": "Indicates errors or critical status." + } ], "default": "default", "description": "Visual treatment. Use destructive to indicate errors or critical status." @@ -694,9 +858,21 @@ "labelRequirement": "none", "notes": "Badge is a presentational element. If the badge conveys status that is not also communicated through surrounding text, use aria-label on the badge or ensure the status is programmatically determinable through other means." }, - "tokens": ["primary", "primary-foreground", "secondary", "secondary-foreground", "destructive", "destructive-foreground", "border"], + "tokens": [ + "primary", + "primary-foreground", + "secondary", + "secondary-foreground", + "destructive", + "destructive-foreground", + "border" + ], "relatedComponents": [], - "tags": ["display", "status", "label"] + "tags": [ + "display", + "status", + "label" + ] }, "dropdown-menu": { "name": "Dropdown Menu", @@ -725,11 +901,26 @@ "accessibility": { "role": "menu", "keyboardInteractions": [ - { "key": "Arrow Down", "description": "Moves focus to the next menu item." }, - { "key": "Arrow Up", "description": "Moves focus to the previous menu item." }, - { "key": "Enter", "description": "Activates the focused menu item." }, - { "key": "Space", "description": "Activates the focused menu item." }, - { "key": "Escape", "description": "Closes the menu and returns focus to the trigger." } + { + "key": "Arrow Down", + "description": "Moves focus to the next menu item." + }, + { + "key": "Arrow Up", + "description": "Moves focus to the previous menu item." + }, + { + "key": "Enter", + "description": "Activates the focused menu item." + }, + { + "key": "Space", + "description": "Activates the focused menu item." + }, + { + "key": "Escape", + "description": "Closes the menu and returns focus to the trigger." + } ], "focusManagement": "On open, focus moves to the first menu item. On close, focus returns to the trigger. Arrow keys cycle through items.", "labelRequirement": "required-accessible-name" @@ -743,7 +934,9 @@ "required": true, "slot": "trigger", "acceptsChildren": "components", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "dropdown-menu-content", @@ -758,14 +951,18 @@ "name": "DropdownMenuItem", "description": "A single actionable item in the menu.", "acceptsChildren": "text", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "dropdown-menu-checkbox-item", "name": "DropdownMenuCheckboxItem", "description": "A menu item that can be toggled on/off.", "acceptsChildren": "text", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "dropdown-menu-radio-group", @@ -778,7 +975,9 @@ "name": "DropdownMenuRadioItem", "description": "A radio-style menu item for single selection within a group.", "acceptsChildren": "text", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "dropdown-menu-label", @@ -809,7 +1008,9 @@ "name": "DropdownMenuSubTrigger", "description": "The item that opens a sub-menu on hover or keyboard navigation.", "acceptsChildren": "text", - "categories": ["interactive"] + "categories": [ + "interactive" + ] }, { "id": "dropdown-menu-sub-content", @@ -819,10 +1020,27 @@ } ] }, - "tokens": ["background", "foreground", "accent", "muted", "muted-foreground", "border", "radius"], - "relatedComponents": ["button"], - "tags": ["interactive", "menu", "overlay"], - "categories": ["interactive", "overlay"] + "tokens": [ + "background", + "foreground", + "accent", + "muted", + "muted-foreground", + "border", + "radius" + ], + "relatedComponents": [ + "button" + ], + "tags": [ + "interactive", + "menu", + "overlay" + ], + "categories": [ + "interactive", + "overlay" + ] }, "table": { "name": "Table", @@ -941,242 +1159,7736 @@ "severity": "must" } ], - "tokens": ["background", "foreground", "muted", "muted-foreground", "border"], - "relatedComponents": ["badge", "button", "dropdown-menu"], - "tags": ["data", "table", "tabular", "display"] - } - }, - "categories": { - "interactive": { - "name": "Interactive", - "description": "Receives pointer or keyboard activation: buttons, inputs, menu items, and trigger/action/cancel sub-components. Category-based composition rules select by this instead of enumerating ids." - }, - "overlay": { - "name": "Overlay", - "description": "Renders content in a layer above the page with its own focus and dismiss semantics: dialogs, alert dialogs, dropdown menus." - } - }, - "patterns": [ - { - "id": "destructive-action-confirmation", - "name": "Destructive Action Confirmation", - "description": "A confirmation flow for actions that are irreversible or have significant consequences, using AlertDialog to require explicit user acknowledgment.", - "intent": "Prevent accidental data loss or irreversible state changes by requiring the user to explicitly confirm before proceeding.", - "context": "Use whenever an action cannot be undone: deleting a record, removing a team member, clearing form data, revoking access, or any operation the user might regret if triggered accidentally.", - "components": ["alert-dialog", "button"], - "guidance": "Use AlertDialog, not Dialog, for destructive confirmations. The trigger should clearly indicate the destructive nature of the action. Inside the AlertDialog, provide a clear title stating what will happen, a description of the consequences, and two actions: a cancel option and a confirm option. The confirm button MUST use the destructive variant. Place the cancel action before the confirm action in the footer. The description should state specifically what will be affected (e.g., 'This will permanently delete 3 projects and all associated data').", - "relatedPatterns": [], - "tags": ["confirmation", "destructive", "modal", "safety"] - }, - { - "id": "form-field-layout", - "name": "Form Field Layout", - "description": "Standard arrangement of form controls within a Card container, providing consistent structure for data entry interfaces.", - "intent": "Give forms a consistent visual structure that groups related inputs, separates them from surrounding content, and provides clear hierarchy through Card sub-components.", - "context": "Use for settings panels, profile editors, checkout flows, or any interface where the user enters structured data across multiple fields.", - "components": ["card", "input", "button"], - "guidance": "Wrap the form in a Card. Use CardHeader with CardTitle and CardDescription to introduce the form's purpose. Place inputs inside CardContent with consistent vertical spacing (space-y-4 or equivalent). Group related fields together. Place primary actions (submit, save) in CardFooter. Labels should be associated with inputs for accessibility. Required fields should be visually indicated.", - "relatedPatterns": ["destructive-action-confirmation"], - "tags": ["form", "layout", "data-entry"] - }, - { - "id": "contextual-actions-menu", - "name": "Contextual Actions Menu", - "description": "A pattern for exposing a set of context-sensitive actions through a dropdown menu triggered by an icon button.", - "intent": "Keep interfaces clean by collecting secondary or overflow actions behind a single trigger, while maintaining discoverability through a familiar interaction pattern.", - "context": "Use for row-level actions in tables or lists, overflow menus in toolbars, or any place where multiple actions apply to a single item but showing all of them inline would be visually noisy.", - "components": ["dropdown-menu", "button"], - "guidance": "Use a Button with the ghost variant and icon size as the DropdownMenuTrigger. A vertical ellipsis icon is the conventional trigger for contextual menus. Group related actions with DropdownMenuGroup and separate groups with DropdownMenuSeparator. Place destructive actions (delete, remove) at the end of the menu, visually distinguished. Include keyboard shortcuts in DropdownMenuItem when available.", - "relatedPatterns": ["destructive-action-confirmation"], - "tags": ["actions", "menu", "overflow"] - }, - { - "id": "data-table-with-row-actions", - "name": "Data Table with Row Actions", - "description": "An enterprise data table that presents records with a sortable header, a status column, and a trailing column of per-row actions — including destructive ones routed through a confirmation. Composes the Table primitives with Badge for status, a DropdownMenu (via the contextual-actions-menu pattern) for the actions column, and AlertDialog (via the destructive-action-confirmation pattern) for destructive actions.", - "intent": "Let users scan and act on a list of records — reviewing each row's status and reaching its actions — without sacrificing the semantics, keyboard access, or safety guarantees that the underlying components provide.", - "context": "Use for access-management and administrative surfaces: a members table, a users-and-roles list, an API-keys table, or any view where each row is an entity with a status and a set of actions, some of which (remove, revoke access) are destructive.", - "components": ["table", "badge", "button", "dropdown-menu", "alert-dialog"], - "guidance": "Build the table from the Table primitives: a TableHeader row of TableHead cells, a TableBody of one TableRow per record, and a TableCaption (or aria-label) naming the table. Make sortable columns real controls — render a ghost Button inside the TableHead and set aria-sort on that TableHead to ascending, descending, or none, with only one column sorted at a time. Render the status column with a Badge whose variant reflects state (for example default or secondary for active and pending, destructive for suspended or revoked); because Badge is presentational, keep the status readable as text so it is announced by assistive technology, not conveyed by color alone. Put per-row actions in a trailing TableCell as a contextual-actions-menu: a ghost icon Button (a vertical ellipsis) as the DropdownMenuTrigger opening a DropdownMenu of actions, with an aria-label on the trigger. Route every destructive row action — remove, revoke access — through the destructive-action-confirmation pattern using AlertDialog, never Dialog, so the action cannot be dismissed by accident; the AlertDialogDescription should name the specific record affected (for example 'This revokes Jordan Lee's access to the Acme workspace'). Do not make the entire TableRow a single clickable target while it also contains its own action controls — that is the nested-interactive-elements anti-pattern, which produces ambiguous click targets and unpredictable behavior for keyboard and screen-reader users; if rows need a primary navigation, expose it as a distinct link or cell control rather than wrapping the row.", - "relatedPatterns": ["contextual-actions-menu", "destructive-action-confirmation"], - "tags": ["table", "data", "actions", "access-management", "destructive"] - } - ], - "antiPatterns": [ - { - "id": "dialog-for-destructive-actions", - "name": "Using Dialog for Destructive Confirmations", - "description": "Using the dismissible Dialog component instead of AlertDialog when confirming a destructive or irreversible action.", - "reason": "Dialog can be dismissed by clicking the overlay or pressing Escape, which means a user can accidentally bypass the confirmation without making a conscious choice. AlertDialog forces the user to select an explicit action (confirm or cancel), providing a stronger safeguard against accidental destructive operations. This distinction also matters for accessibility: screen readers announce AlertDialog with greater urgency.", - "severity": "must-not", - "insteadUse": "destructive-action-confirmation", - "components": ["dialog", "alert-dialog"], - "tags": ["modal", "destructive", "accessibility"] - }, - { - "id": "nested-interactive-elements", - "name": "Nesting Interactive Elements", - "description": "Placing a clickable or focusable element (Button, Link, Input) inside another clickable element (a clickable Card, a Button, or a Link).", - "reason": "Nested interactive elements create ambiguous click targets and are a significant accessibility violation. Screen readers cannot determine user intent when interactive elements are nested, and the resulting behavior varies unpredictably across browsers. The outer element may intercept clicks intended for the inner element, or both may fire, leading to unexpected navigation or duplicate actions.", - "severity": "must-not", - "components": ["button", "card", "input"], - "tags": ["accessibility", "interaction", "html-semantics"] - }, - { - "id": "div-as-button", - "name": "Using a Styled Div as a Button", - "description": "Creating a clickable element by adding an onClick handler and visual styles to a
or instead of using the Button component or a native