diff --git a/examples/shadcn-ui.dspack.json b/examples/shadcn-ui.dspack.json
index 6ebd8d7..838263e 100644
--- a/examples/shadcn-ui.dspack.json
+++ b/examples/shadcn-ui.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.0.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,7700 @@
"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