Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion apps/ui/skills/coss/references/primitives/alert-dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,68 @@ import {
</AlertDialog>
```

`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={<Button ... />}` for cancel/confirm actions to preserve button semantics and styling.
- **Action composition**: use `AlertDialogClose render={<Button ... />}` for Cancel and for synchronous confirms to preserve button semantics and styling.
- **Destructive affordance**: pair destructive trigger/confirm variants (`destructive-outline`, `destructive`) for clear risk signaling.
- **Footer variants**: use `AlertDialogFooter variant="bare"` when border/background framing should be removed.
- **Close confirmation chain**: for unsaved changes in broader workflows, pair with dialog flows like `p-dialog-4`.

### Key patterns

Async confirmation (the dialog stays open until the mutation settles):

```tsx
const [open, setOpen] = useState(false)
const [isPending, setIsPending] = useState(false)

async function handleDelete() {
setIsPending(true)
try {
await deleteAccount()
setOpen(false)
} catch {
// Keep the dialog open so the user can retry; surface the error (e.g. a toast).
} finally {
setIsPending(false)
}
}

<AlertDialog onOpenChange={setOpen} open={open}>
<AlertDialogTrigger render={<Button variant="destructive-outline" />}>
Delete Account
</AlertDialogTrigger>
<AlertDialogPopup>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogClose render={<Button variant="ghost" />}>Cancel</AlertDialogClose>
<Button loading={isPending} onClick={handleDelete} variant="destructive">
Delete Account
</Button>
</AlertDialogFooter>
</AlertDialogPopup>
</AlertDialog>
```

- 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.
Expand Down
1 change: 1 addition & 0 deletions apps/ui/skills/coss/references/primitives/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
44 changes: 43 additions & 1 deletion apps/ui/skills/coss/references/primitives/card.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
<CardFrame>
<CardFrameHeader>
<CardFrameTitle>Projects</CardFrameTitle>
<CardFrameDescription>Manage your projects</CardFrameDescription>
<CardFrameAction>
<Button variant="outline">
<PlusIcon aria-hidden="true" />
Add
</Button>
</CardFrameAction>
</CardFrameHeader>
<Card>
<CardPanel>Content</CardPanel>
</Card>
<CardFrameFooter>Footnote or secondary info</CardFrameFooter>
</CardFrame>
```

- 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`
5 changes: 4 additions & 1 deletion apps/ui/skills/coss/references/primitives/drawer.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ Drawer with a visible drag handle:
{/* Form content */}
</DrawerPanel>
<DrawerFooter>
<Button>Save</Button>
<DrawerClose render={<Button variant="ghost" />}>Cancel</DrawerClose>
<Button>Save</Button>
</DrawerFooter>
</DrawerPopup>
</Drawer>
```

**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
Expand All @@ -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 `<form>`; prefer **header outside**, **`Form className="contents"`** around **panel + footer** (see dialog/form skills).

## Useful particle references
Expand Down
32 changes: 30 additions & 2 deletions apps/ui/skills/coss/references/primitives/field.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Field name="email">
<FieldLabel>Email *</FieldLabel>
<FieldLabel>
Email <span className="text-destructive-foreground">*</span>
</FieldLabel>
<Input type="email" required placeholder="name@company.com" />
<FieldDescription>We'll never share your email.</FieldDescription>
<FieldError>Please enter a valid email.</FieldError>
</Field>
```

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<Values>({
resolver: zodResolver(schema),
})

<form noValidate onSubmit={handleSubmit(onSubmit)}>
<Field invalid={!!errors.email} name="email">
<FieldLabel>
Email <span className="text-destructive-foreground">*</span>
</FieldLabel>
<Input type="email" {...register("email")} placeholder="name@company.com" />
<FieldError>{errors.email?.message}</FieldError>
</Field>
</form>
```

- 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 `<form noValidate>`. 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
Expand All @@ -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
Expand Down
30 changes: 29 additions & 1 deletion apps/ui/skills/coss/references/primitives/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<>
<Menu>
<MenuTrigger render={<Button aria-label="Project actions" size="icon" variant="ghost" />}>
<EllipsisIcon aria-hidden="true" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>Rename</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuSeparator />
<MenuItem onClick={() => setConfirmOpen(true)} variant="destructive">
Delete
</MenuItem>
</MenuPopup>
</Menu>
<AlertDialog onOpenChange={setConfirmOpen} open={confirmOpen}>
<AlertDialogPopup>{/* header + footer, see alert-dialog.md */}</AlertDialogPopup>
</AlertDialog>
</>
```

## Patterns from coss particles

- **Portal forwarding**: optional `portalProps` on `MenuPopup` → Base UI `Menu.Portal` (`keepMounted`, `container`, …). See [portal-props.md](../portal-props.md).
Expand All @@ -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={<DrawerMenuItem />}` when selection should dismiss the drawer.

Expand All @@ -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

Expand Down
17 changes: 17 additions & 0 deletions apps/ui/skills/coss/references/primitives/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ Tooltip on an icon-only button:
</Tooltip>
```

Tooltip explaining why a button is disabled:

```tsx
<Tooltip>
<TooltipTrigger render={<span className="inline-flex" tabIndex={0} />}>
<Button disabled>Publish</Button>
</TooltipTrigger>
<TooltipPopup>Add a title before publishing</TooltipPopup>
</Tooltip>
```

- A disabled `Button` sets the native `disabled` attribute and `pointer-events-none`, so it never receives hover or focus. Putting `TooltipTrigger render={<Button disabled />}` directly on it means the tooltip never opens.
- Render the trigger as a wrapper `span` instead (`TooltipTrigger` renders a `<button>` by default, which cannot contain another button). Hover passes through the disabled button to the span.
- `tabIndex={0}` keeps the reason reachable by keyboard, since the disabled button is skipped in tab order. Use `inline-flex` (or `block`/`w-full` for full-width buttons) so the wrapper matches the button's box.
- When the button becomes enabled, drop the wrapper or render the tooltip only while disabled, so the span does not add an extra tab stop.

Grouped tooltips (shared delay/provider):

```tsx
Expand All @@ -87,6 +103,7 @@ See `p-tooltip-1` through `p-tooltip-3` for basic, grouped, and animated tooltip
- Placing interactive controls inside tooltip content (tooltip should stay informational).
- Relying on tooltip as sole label for icon-only controls (still provide accessible name).
- Using tooltip for long-form content that should be popover/dialog.
- Attaching a tooltip trigger directly to a disabled button; wrap it in a focusable `span` trigger instead (see above).

## Useful particle references

Expand Down
Loading