From d542a27b10f01f0216a25e1c4dbb3494849aed7a Mon Sep 17 00:00:00 2001 From: pasqualevitiello Date: Fri, 16 Oct 2026 14:18:40 +0200 Subject: [PATCH] docs(skill): fix footer order, async confirms, and field validation guidance - drawer: put Cancel before the primary action in the footer example and explain the flex-col-reverse / sm:flex-row ordering. - alert-dialog: add the async confirmation pattern (controlled open, confirm as a loading Button, close on success, Cancel stays AlertDialogClose). - card: document CardAction and the CardFrame family (header, title, description, action, footer) with particle references. - field: show the React Hook Form variant (noValidate, no constraint attributes, invalid + FieldError children) and explain why native or Base UI constraint validation blocks the library's submit handler. - tooltip/button: document the span-wrapper trigger for explaining a disabled button, since disabled buttons get no hover or focus. - menu: destructive items go last after a separator; confirms open a sibling AlertDialog controlled by state, never one nested in MenuPopup. Co-Authored-By: Claude Opus 5.5 --- .../references/primitives/alert-dialog.md | 51 ++++++++++++++++++- .../coss/references/primitives/button.md | 1 + .../skills/coss/references/primitives/card.md | 44 +++++++++++++++- .../coss/references/primitives/drawer.md | 5 +- .../coss/references/primitives/field.md | 32 +++++++++++- .../skills/coss/references/primitives/menu.md | 30 ++++++++++- .../coss/references/primitives/tooltip.md | 17 +++++++ 7 files changed, 174 insertions(+), 6 deletions(-) diff --git a/apps/ui/skills/coss/references/primitives/alert-dialog.md b/apps/ui/skills/coss/references/primitives/alert-dialog.md index 2d09bfecf..70f825060 100644 --- a/apps/ui/skills/coss/references/primitives/alert-dialog.md +++ b/apps/ui/skills/coss/references/primitives/alert-dialog.md @@ -62,19 +62,68 @@ import { ``` +`AlertDialogClose` closes the dialog as soon as it is clicked, so use it as the confirm only when the action is synchronous. For async work, see **Async confirmation** below. + ## Patterns from coss particles - **Portal forwarding**: optional `portalProps` on `AlertDialogPopup` → Base UI `AlertDialog.Portal` (`keepMounted`, `container`, …). See [portal-props.md](../portal-props.md). - **Section structure**: keep `AlertDialogHeader` and `AlertDialogFooter` as direct sections of `AlertDialogPopup` (there is no `AlertDialogPanel`; add a `div` or fragment between them only if you need extra body content). -- **Action composition**: use `AlertDialogClose render={ + + + +``` + +- The confirm is a plain `Button` (not `AlertDialogClose`) with `loading`, which disables it and shows the spinner while the request runs. +- Close the dialog from the success path (`setOpen(false)`); on failure keep it open so the user can retry. +- Cancel stays an `AlertDialogClose` ghost button. Keep Cancel first in source: the footer is `flex-col-reverse` / `sm:flex-row sm:justify-end`, so the confirm renders on the right (or on top when stacked). +- With a query library, use its pending state (for example `mutation.isPending`) instead of local state. + ## Common pitfalls - Using AlertDialog as a generic content modal instead of high-risk confirmation UI. - Omitting explicit destructive/cancel action distinction. +- Confirming an async action with `AlertDialogClose`: the dialog closes before the mutation settles, so there is no loading state and nowhere to show a failure. - Wrapping dialog sections in extra containers that break built-in layout (use `className="contents"` only when needed). - Mixing Dialog/Popover composition APIs without validating this primitive's parts. - Skipping focus-return and escape-key verification on real trigger flows. diff --git a/apps/ui/skills/coss/references/primitives/button.md b/apps/ui/skills/coss/references/primitives/button.md index 825fd1e10..bac8fd469 100644 --- a/apps/ui/skills/coss/references/primitives/button.md +++ b/apps/ui/skills/coss/references/primitives/button.md @@ -99,6 +99,7 @@ Prefer the `loading` prop for typical async actions. Use the composite approach - Omitting explicit `type` inside forms/dialogs and triggering unintended submit behavior. - Using icon-only buttons without `aria-label` on the button. +- Expecting a disabled button to show a tooltip: `disabled` sets the native attribute plus `pointer-events-none`, so the button gets no hover or focus. Wrap it in a focusable `span` rendered as the `TooltipTrigger` (see [tooltip.md](./tooltip.md)). - Rebuilding button state styling with ad-hoc classes instead of variants/sizes. - Using `SelectButton` as if it were a general-purpose `Button`; `SelectButton` is a select-flavored trigger helper and should be treated as a `select`/`combobox` pattern. diff --git a/apps/ui/skills/coss/references/primitives/card.md b/apps/ui/skills/coss/references/primitives/card.md index 40d79b614..ea597a4d6 100644 --- a/apps/ui/skills/coss/references/primitives/card.md +++ b/apps/ui/skills/coss/references/primitives/card.md @@ -22,14 +22,23 @@ Manual deps from docs: ```tsx import { Card, + CardAction, CardDescription, CardFooter, + CardFrame, + CardFrameAction, + CardFrameDescription, + CardFrameFooter, + CardFrameHeader, + CardFrameTitle, CardHeader, CardPanel, CardTitle, } from "@/components/ui/card" ``` +`CardPanel` is also exported as `CardContent`. + ## Minimal pattern ```tsx @@ -70,16 +79,49 @@ Card with form content: Keep `CardHeader`, `CardPanel`, and `CardFooter` as direct children of `Card` to preserve built-in spacing and layout. +Put header actions in `CardAction`, a sibling of `CardTitle` and `CardDescription` inside `CardHeader`; it is placed top-right via the header grid. + +### CardFrame + +`CardFrame` is a muted outer shell with its own header and footer around one or more inner `Card`s (or a `Table variant="card"`). Use it when a section needs a title row, an optional action, and a footnote outside the main surface. + +```tsx + + + Projects + Manage your projects + + + + + + Content + + Footnote or secondary info + +``` + +- Keep `CardFrameHeader`, the inner `Card`(s), and `CardFrameFooter` as direct children of `CardFrame`; the frame clips, rounds, and removes shadows on direct `Card` children only. +- `CardFrameAction` goes inside `CardFrameHeader` as a sibling of the title and description; it sits in the second grid column, vertically centered. +- Do not use `CardHeader`/`CardFooter` on the frame itself; those belong to the inner `Card`. +- For tables, render `Table variant="card"` as a direct child of `CardFrame` (see table guide, `p-table-6` through `p-table-8`). + ### More examples -See `p-card-1` through `p-card-8` for various card compositions. +See `p-card-1` through `p-card-11` for various card compositions. ## Common pitfalls - Skipping `CardHeader`/`CardPanel`/`CardFooter` structure in composed cards. - Mixing unrelated layout wrappers that break spacing between card sections. - Using cards as generic wrappers when `Frame` or plain layout would be clearer. +- Nesting a `Card` inside another wrapper within `CardFrame`, which skips the frame's clipping and radius rules for direct `Card` children. ## Useful particle references - core patterns: `p-card-1`, `p-card-2`, `p-card-3`, `p-card-4`, `p-card-5`, `p-card-6`, `p-card-7`, `p-card-8` +- `CardFrame` with footer: `p-card-4`; with header: `p-card-5`; header + footer: `p-card-6`, `p-card-7` +- `CardFrame` with header action: `p-card-11` diff --git a/apps/ui/skills/coss/references/primitives/drawer.md b/apps/ui/skills/coss/references/primitives/drawer.md index e48869b21..a7be2a2a6 100644 --- a/apps/ui/skills/coss/references/primitives/drawer.md +++ b/apps/ui/skills/coss/references/primitives/drawer.md @@ -87,13 +87,15 @@ Drawer with a visible drag handle: {/* Form content */} - }>Cancel + ``` +**Footer action order**: write Cancel first and the primary action last. `DrawerFooter` is `flex-col-reverse` on mobile and `sm:flex-row sm:justify-end` from `sm` up, so source order puts the primary action on the right on desktop and on top when stacked. The same applies to `DialogFooter` and `AlertDialogFooter`. + Responsive drawer + dialog (drawer on mobile, dialog on desktop): see `p-drawer-12`. ### More examples @@ -104,6 +106,7 @@ See `p-drawer-1` through `p-drawer-13` for inset, straight, scrollable, nested, - Choosing Sheet for desktop side panels; use Drawer. Use Dialog for centered modal flows. - Forgetting responsive switch logic when drawer is mobile-only variant. +- Putting the primary action before Cancel in `DrawerFooter`; the reversed mobile stack then puts Cancel on top and the primary on the left on desktop. - Breaking section layout by putting the whole dialog in a block-level `
`; prefer **header outside**, **`Form className="contents"`** around **panel + footer** (see dialog/form skills). ## Useful particle references diff --git a/apps/ui/skills/coss/references/primitives/field.md b/apps/ui/skills/coss/references/primitives/field.md index 9c18e7f9d..dedd6651e 100644 --- a/apps/ui/skills/coss/references/primitives/field.md +++ b/apps/ui/skills/coss/references/primitives/field.md @@ -50,17 +50,44 @@ import { Input } from "@/components/ui/input" ### Key patterns -Required field with error: +Required field with error (Base UI constraint validation): ```tsx - Email * + + Email * + We'll never share your email. Please enter a valid email. ``` +Native constraint attributes (`required`, `type="email"`, `min`, `pattern`, …) are the validation source here: `Field` reads the control's `ValidityState` and `FieldError` renders when it fails. Use this only when the browser constraints are the single source of truth. + +Required field with React Hook Form (or another schema/form library): + +```tsx +const { formState: { errors }, handleSubmit, register } = useForm({ + resolver: zodResolver(schema), +}) + + + + + Email * + + + {errors.email?.message} + + +``` + +- Let the library own validation: drop `required` (and other constraint attributes like `min`/`max`/`pattern`) from the control and express them in the schema. +- Use a plain `
`. Without `noValidate`, the browser's native validation blocks submit and shows its own bubble before `handleSubmit` runs; with it, `type="email"` can stay for the mobile keyboard and autofill. +- If you wrap the fields in coss `Form` instead: it already sets `noValidate`, but each `Field` inside it still runs native constraint checks on submit (including `type="email"` mismatches) and cancels the submit, focusing the first invalid control, before your `onSubmit` handler fires. Remove every constraint attribute in that case (use `inputMode="email"` for the keyboard). +- Drive the error state from the library: `invalid` on `Field` and the message as `FieldError` children. The asterisk is visual only; the schema enforces it. + Field wrapping an autocomplete: ```tsx @@ -86,6 +113,7 @@ See `p-field-1` through `p-field-9` for required, disabled, error, validity, inp - Rendering errors detached from the related control, breaking context. - Missing `name` in form flows, causing silent submit omissions. +- Mixing native constraint attributes (`required`, `min`, `pattern`) with React Hook Form or another schema validator; native/Base UI validation runs first and the library's errors never show. - Using field wrapper without corresponding label/description/error semantics. ## Useful particle references diff --git a/apps/ui/skills/coss/references/primitives/menu.md b/apps/ui/skills/coss/references/primitives/menu.md index f1be561b9..f1e84112f 100644 --- a/apps/ui/skills/coss/references/primitives/menu.md +++ b/apps/ui/skills/coss/references/primitives/menu.md @@ -62,6 +62,31 @@ import { Use popup positioning props like `align` / `sideOffset` only when a layout needs explicit tuning. +Destructive item with confirmation: + +```tsx +const [confirmOpen, setConfirmOpen] = useState(false) + +<> + + }> + + + Rename + Duplicate + + setConfirmOpen(true)} variant="destructive"> + Delete + + + + + {/* header + footer, see alert-dialog.md */} + + +``` + ## Patterns from coss particles - **Portal forwarding**: optional `portalProps` on `MenuPopup` → Base UI `Menu.Portal` (`keepMounted`, `container`, …). See [portal-props.md](../portal-props.md). @@ -72,7 +97,8 @@ Use popup positioning props like `align` / `sideOffset` only when a layout needs - Use `MenuCheckboxItem variant="switch"` for toggle-style preferences. - Use `MenuRadioGroup` + `MenuRadioItem` with a `defaultValue` when enforcing single-choice selection. - Use `MenuShortcut` to display keyboard hints in dense command menus. -- Use `variant="destructive"` on dangerous actions. +- Use `variant="destructive"` on dangerous actions, and put them last, after a `MenuSeparator`, so they are visually apart from routine actions and harder to hit by accident. +- When an item needs confirmation (e.g. Delete), the item only sets state; render the `AlertDialog` (or `Dialog`) as a sibling of `Menu`, controlled by `open` / `onOpenChange`. Never nest the dialog inside `MenuPopup`: the popup unmounts when the menu closes, taking the dialog with it. See `p-dialog-2`. - For responsive action menus, keep desktop on `Menu` and switch mobile to `DrawerMenu` / `DrawerMenuTrigger` / `DrawerMenuItem` patterns. - In `DrawerMenu` flows, wrap actionable rows with `DrawerClose render={}` when selection should dismiss the drawer. @@ -81,6 +107,8 @@ Use popup positioning props like `align` / `sideOffset` only when a layout needs - Forgetting `MenuGroup` around grouped structures. - Missing submenu pair (`MenuSubTrigger` + `MenuSubPopup`) for nested actions. - Mixing navigation and action items without clear close behavior (`closeOnClick`) and semantics. +- Placing destructive items in the middle of the list or without a separator. +- Nesting an `AlertDialog`/`Dialog` (or its trigger) inside `MenuPopup` for a confirm step; keep it a sibling of `Menu` driven by state. ## Useful particle references diff --git a/apps/ui/skills/coss/references/primitives/tooltip.md b/apps/ui/skills/coss/references/primitives/tooltip.md index ca5d5a834..4e6bd2d47 100644 --- a/apps/ui/skills/coss/references/primitives/tooltip.md +++ b/apps/ui/skills/coss/references/primitives/tooltip.md @@ -63,6 +63,22 @@ Tooltip on an icon-only button: ``` +Tooltip explaining why a button is disabled: + +```tsx + + }> + + + Add a title before publishing + +``` + +- A disabled `Button` sets the native `disabled` attribute and `pointer-events-none`, so it never receives hover or focus. Putting `TooltipTrigger render={