diff --git a/packages/propel/src/components/alert-dialog/alert-dialog.stories.tsx b/packages/propel/src/components/alert-dialog/alert-dialog.stories.tsx index bc5c9e67..c27bdb54 100644 --- a/packages/propel/src/components/alert-dialog/alert-dialog.stories.tsx +++ b/packages/propel/src/components/alert-dialog/alert-dialog.stories.tsx @@ -1,12 +1,17 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import { TriangleAlert } from "lucide-react"; import { expect, userEvent, waitFor, within } from "storybook/test"; import { Button } from "../../ui/button"; import { AlertDialog, + AlertDialogActions, AlertDialogClose, AlertDialogContent, AlertDialogDescription, + AlertDialogHeader, + AlertDialogIcon, + AlertDialogIntro, AlertDialogTitle, AlertDialogTrigger, } from "./index"; @@ -23,8 +28,12 @@ const meta = { subcomponents: { AlertDialogTrigger, AlertDialogContent, + AlertDialogHeader, + AlertDialogIcon, + AlertDialogIntro, AlertDialogTitle, AlertDialogDescription, + AlertDialogActions, AlertDialogClose, }, } satisfies Meta; @@ -40,29 +49,26 @@ export const Default: Story = { Delete project - {/* - * Two layout groups, separated by the parent's gap (never a margin on a - * child): an "intro" (title + description) and the "actions" row. These - * boundaries are the future anatomy surfaces — e.g. AlertDialogIntro and - * AlertDialogActions — so define them correctly here before hardening. - */} -
-
+ + + + + Delete project? This permanently removes the project and all of its work items. This action can't be undone. -
-
- - -
-
+ + + + + +
), diff --git a/packages/propel/src/components/alert-dialog/index.tsx b/packages/propel/src/components/alert-dialog/index.tsx index 4cfb2aa9..039ba639 100644 --- a/packages/propel/src/components/alert-dialog/index.tsx +++ b/packages/propel/src/components/alert-dialog/index.tsx @@ -3,10 +3,19 @@ export { AlertDialogContent, type AlertDialogContentProps } from "./alert-dialog export { AlertDialog, type AlertDialogProps, + AlertDialogActions, + type AlertDialogActionsProps, AlertDialogClose, type AlertDialogCloseProps, AlertDialogDescription, type AlertDialogDescriptionProps, + AlertDialogHeader, + type AlertDialogHeaderProps, + AlertDialogIcon, + type AlertDialogIconProps, + type AlertDialogIconTone, + AlertDialogIntro, + type AlertDialogIntroProps, AlertDialogTitle, type AlertDialogTitleProps, AlertDialogTrigger, diff --git a/packages/propel/src/ui/alert-dialog/alert-dialog-actions.tsx b/packages/propel/src/ui/alert-dialog/alert-dialog-actions.tsx new file mode 100644 index 00000000..64aebf1d --- /dev/null +++ b/packages/propel/src/ui/alert-dialog/alert-dialog-actions.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { alertDialogActionsVariants } from "./variants"; + +export type AlertDialogActionsProps = React.HTMLAttributes; + +/** + * Right-aligns action buttons with consistent horizontal spacing. The placement and gap are always + * the same — baked in by the design system. + */ +export function AlertDialogActions({ ...props }: AlertDialogActionsProps) { + return
; +} diff --git a/packages/propel/src/ui/alert-dialog/alert-dialog-header.tsx b/packages/propel/src/ui/alert-dialog/alert-dialog-header.tsx new file mode 100644 index 00000000..57786cca --- /dev/null +++ b/packages/propel/src/ui/alert-dialog/alert-dialog-header.tsx @@ -0,0 +1,17 @@ +import type * as React from "react"; + +import { alertDialogHeaderVariants } from "./variants"; + +export type AlertDialogHeaderProps = Omit< + React.ComponentPropsWithoutRef<"div">, + "className" | "style" +>; + +/** + * The top region of the popup: lays out the leading `AlertDialogIcon` at the inline-start of the + * `AlertDialogIntro` (icon left of title), per the design spec. The icon and intro are passed as + * children. + */ +export function AlertDialogHeader(props: AlertDialogHeaderProps) { + return
; +} diff --git a/packages/propel/src/ui/alert-dialog/alert-dialog-icon.tsx b/packages/propel/src/ui/alert-dialog/alert-dialog-icon.tsx new file mode 100644 index 00000000..bf516467 --- /dev/null +++ b/packages/propel/src/ui/alert-dialog/alert-dialog-icon.tsx @@ -0,0 +1,26 @@ +import { type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { alertDialogIconVariants } from "./variants"; + +export type AlertDialogIconTone = NonNullable["tone"]>; + +export type AlertDialogIconProps = Omit< + React.ComponentPropsWithoutRef<"span">, + "className" | "style" +> & { + /** + * Intent of the alert, the destructive-vs-informational axis the spec marks adjustable. Drives + * the icon's color; required so the caller always states the intent. + */ + tone: AlertDialogIconTone; +}; + +/** + * The decorative leading glyph shown at the inline-start of the title. Sizes its single child to + * `--node-size`, so the caller passes a bare icon (warning, error, info, …). Decorative — the title + * carries the accessible name — so it is `aria-hidden`. + */ +export function AlertDialogIcon({ tone, ...props }: AlertDialogIconProps) { + return ; +} diff --git a/packages/propel/src/ui/alert-dialog/alert-dialog-intro.tsx b/packages/propel/src/ui/alert-dialog/alert-dialog-intro.tsx new file mode 100644 index 00000000..0f96c134 --- /dev/null +++ b/packages/propel/src/ui/alert-dialog/alert-dialog-intro.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { alertDialogIntroVariants } from "./variants"; + +export type AlertDialogIntroProps = React.HTMLAttributes; + +/** + * Groups the title and description with consistent vertical spacing. The gap between title and + * description is always the same — baked in by the design system. + */ +export function AlertDialogIntro({ ...props }: AlertDialogIntroProps) { + return
; +} diff --git a/packages/propel/src/ui/alert-dialog/alert-dialog.stories.tsx b/packages/propel/src/ui/alert-dialog/alert-dialog.stories.tsx index 9c48b16c..e99c8e84 100644 --- a/packages/propel/src/ui/alert-dialog/alert-dialog.stories.tsx +++ b/packages/propel/src/ui/alert-dialog/alert-dialog.stories.tsx @@ -1,12 +1,17 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import { TriangleAlert } from "lucide-react"; import { expect, userEvent, waitFor, within } from "storybook/test"; import { Button } from "../button"; import { AlertDialog, + AlertDialogActions, AlertDialogBackdrop, AlertDialogClose, AlertDialogDescription, + AlertDialogHeader, + AlertDialogIcon, + AlertDialogIntro, AlertDialogPopup, AlertDialogPortal, AlertDialogTitle, @@ -30,8 +35,12 @@ const meta = { AlertDialogBackdrop, AlertDialogViewport, AlertDialogPopup, + AlertDialogHeader, + AlertDialogIcon, + AlertDialogIntro, AlertDialogTitle, AlertDialogDescription, + AlertDialogActions, AlertDialogClose, }, } satisfies Meta; @@ -50,38 +59,30 @@ export const Anatomy: Story = { - {/* - * Two layout groups, separated by the parent's gap (never a margin on a - * child): an "intro" (title + description) and the "actions" row. These - * boundaries are the future anatomy surfaces — e.g. AlertDialogIntro and - * AlertDialogActions — so define them correctly here before hardening. - */} -
-
+ + + + + Delete account? This permanently deletes your account and cannot be undone. -
-
- - -
-
+ + + + + +
diff --git a/packages/propel/src/ui/alert-dialog/index.tsx b/packages/propel/src/ui/alert-dialog/index.tsx index b94d221b..79aa3d82 100644 --- a/packages/propel/src/ui/alert-dialog/index.tsx +++ b/packages/propel/src/ui/alert-dialog/index.tsx @@ -1,10 +1,18 @@ export { AlertDialog, type AlertDialogProps } from "./alert-dialog"; +export { AlertDialogActions, type AlertDialogActionsProps } from "./alert-dialog-actions"; export { AlertDialogBackdrop, type AlertDialogBackdropProps } from "./alert-dialog-backdrop"; export { AlertDialogClose, type AlertDialogCloseProps } from "./alert-dialog-close"; export { AlertDialogDescription, type AlertDialogDescriptionProps, } from "./alert-dialog-description"; +export { AlertDialogHeader, type AlertDialogHeaderProps } from "./alert-dialog-header"; +export { + AlertDialogIcon, + type AlertDialogIconProps, + type AlertDialogIconTone, +} from "./alert-dialog-icon"; +export { AlertDialogIntro, type AlertDialogIntroProps } from "./alert-dialog-intro"; export { AlertDialogPopup, type AlertDialogPopupProps } from "./alert-dialog-popup"; export { AlertDialogPortal } from "./alert-dialog-portal"; export { AlertDialogTitle, type AlertDialogTitleProps } from "./alert-dialog-title"; diff --git a/packages/propel/src/ui/alert-dialog/variants.ts b/packages/propel/src/ui/alert-dialog/variants.ts index 25a2bb93..dfe7debe 100644 --- a/packages/propel/src/ui/alert-dialog/variants.ts +++ b/packages/propel/src/ui/alert-dialog/variants.ts @@ -1,11 +1,14 @@ import { cva, cx } from "class-variance-authority"; +import { nodeSlotClass } from "../../internal/node-slot"; + // AlertDialog is a structural overlay primitive. It is always modal and // non-dismissible, and Base UI drives every interactive state (open/closed, -// starting/ending transition styles) as data attributes — so there are no -// styling axes (variant/tone/magnitude) to expose. The cva pairings below hold -// the static chrome so each part is styled in one place, with no `className` at -// the boundary. Root, Trigger, and Portal carry no styling. +// starting/ending transition styles) as data attributes. The one design axis the +// spec calls out is the leading icon's intent (destructive vs informational), which +// the `AlertDialogIcon` exposes as a required `tone` — every other part is static +// chrome held in a single cva so there is no `className` at the boundary. Root, +// Trigger, and Portal carry no styling. export const alertDialogBackdropVariants = cva( cx( @@ -20,6 +23,9 @@ export const alertDialogViewportVariants = cva( export const alertDialogPopupVariants = cva( cx( + // Fixed width and layout: the max-width constraint and internal spacing are + // always the same per spec — baked here, not left to the consumer. + "flex w-80 flex-col gap-4", "rounded-lg border-sm border-subtle bg-layer-1 p-4 shadow-overlay-100 outline-none", "origin-(--transform-origin) transition-[opacity,transform] duration-200", "data-starting-style:scale-95 data-starting-style:opacity-0", @@ -31,6 +37,36 @@ export const alertDialogTitleVariants = cva("text-16 font-semibold text-primary" export const alertDialogDescriptionVariants = cva("text-14 text-secondary"); +// Header: the top region that places the leading icon at the inline-start of the +// intro (icon left of title, per spec). Items align to the start so a multi-line +// description keeps the icon level with the title. +export const alertDialogHeaderVariants = cva("flex items-start gap-3"); + +// Icon: the decorative leading glyph beside the title. Sizes its single child to +// `--node-size` (via the shared node-slot class) and tints it by `tone` — the +// destructive-vs-informational axis the spec marks as adjustable. The glyph itself +// (warning/error/info) is whatever child the caller passes. No default tone: the +// caller must state the intent. +export const alertDialogIconVariants = cva(cx(nodeSlotClass, "mt-0.5 [--node-size:1.25rem]"), { + variants: { + tone: { + danger: "text-icon-danger", + warning: "text-icon-warning-primary", + info: "text-icon-info-primary", + success: "text-icon-success-primary", + }, + }, +}); + +// Intro: groups the title and description with consistent vertical spacing. +// Always the same per spec (spacing between title and description). `min-w-0` lets +// long copy wrap instead of pushing the icon out of the header row. +export const alertDialogIntroVariants = cva("flex min-w-0 flex-col gap-2"); + +// Actions: right-aligns action buttons with consistent horizontal spacing. +// Always the same per spec (action button placement, right-aligned in footer). +export const alertDialogActionsVariants = cva("flex justify-end gap-2"); + export const alertDialogCloseVariants = cva( cx( "inline-flex items-center justify-center rounded-md text-icon-secondary outline-none",