diff --git a/.envrc b/.envrc index dff05c7f..aaf702fa 100644 --- a/.envrc +++ b/.envrc @@ -1,7 +1,7 @@ # hide warnings from direnv # see https://github.com/direnv/direnv/issues/419#issuecomment-442005962 # see: https://direnv.net/man/direnv.toml.1.html#codewarntimeoutcode -export DIRENV_WARN_TIMEOUT=876000h +export DIRENV_WARN_TIMEOUT=876000h # 100 years in hours export NVM_DIR="$HOME/.nvm" diff --git a/apps/www/app/components/layout.tsx b/apps/www/app/components/layout.tsx index 41d6ec3b..6c05eac4 100644 --- a/apps/www/app/components/layout.tsx +++ b/apps/www/app/components/layout.tsx @@ -154,6 +154,7 @@ const prodReadyComponents = [ */ const previewComponents = [ //, + "AlertDialog", "Calendar", "Pagination", "Popover", @@ -191,6 +192,7 @@ const prodReadyComponentRouteLookup = { } as const satisfies Record<(typeof prodReadyComponents)[number], Route>; const previewComponentsRouteLookup = { + AlertDialog: "/components/preview/alert-dialog", Calendar: "/components/preview/calendar", Pagination: "/components/preview/pagination", Popover: "/components/preview/popover", diff --git a/apps/www/app/routes/components.preview.alert-dialog.tsx b/apps/www/app/routes/components.preview.alert-dialog.tsx new file mode 100644 index 00000000..1ba30020 --- /dev/null +++ b/apps/www/app/routes/components.preview.alert-dialog.tsx @@ -0,0 +1,353 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogBody, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogIcon, + AlertDialogTitle, + AlertDialogTrigger, +} from "@ngrok/mantle/alert-dialog"; +import { Anchor } from "@ngrok/mantle/anchor"; +import { Button } from "@ngrok/mantle/button"; +import { CodeBlock, CodeBlockBody, CodeBlockCode, CodeBlockCopyButton, fmtCode } from "@ngrok/mantle/code-block"; +import { InlineCode } from "@ngrok/mantle/inline-code"; +import type { HeadersFunction, MetaFunction } from "@remix-run/node"; +import { PreviewBadge } from "~/components/badges"; +import { Example } from "~/components/example"; +import { Link } from "~/components/link"; +import { + PropDefaultValueCell, + PropDescriptionCell, + PropNameCell, + PropRow, + PropsTable, + PropTypeCell, + StringPropType, +} from "~/components/props-table"; + +export const meta: MetaFunction = () => { + return [ + { title: "@ngrok/mantle — AlertDialog" }, + { name: "description", content: "mantle is ngrok's UI library and design system" }, + ]; +}; + +export const headers: HeadersFunction = () => { + return { + "Cache-Control": "max-age=300, stale-while-revalidate=604800", + }; +}; + +export default function Page() { + return ( +
+
+
+

+ Alert Dialog +

+ +
+

+ A modal dialog that interrupts the user with important content and expects a response. +

+
+ + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account and remove your data from + our servers. + + + + Cancel + Continue + + + + + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account and remove your data from + our servers. + + + + Cancel + Continue + + + + + + + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account and remove your data from + our servers. + + + + Cancel + Continue + + + + `} + /> + + +
+
+ +
+
+

+ API Reference +

+

+ The AlertDialog components are built on top of{" "} + + Radix Alert Dialog + + . +

+
+ +
+
+

AlertDialog

+ +

The root component for the Alert Dialog.

+

+ All props from Radix{" "} + + AlertDialog.Root + + , plus: +

+
+ + + + + +
    +
  • + +
  • +
  • + +
  • +
+
+ + +

+ Indicates the importance or impact level of the AlertDialog, affecting its color and styling to + communicate its purpose to the user. +

+
+
+
+
+ +
+

AlertDialogTrigger

+ +

A button that opens the Alert Dialog.

+

+ Radix{" "} + + AlertDialog.Trigger + {" "} + props. +

+
+ +
+

AlertDialogContent

+ +

+ The popover Alert Dialog container. Renders on top of the overlay and is centered in the viewport. +

+

+ Radix{" "} + + AlertDialog.Content + {" "} + props. +

+
+ +
+

AlertDialogHeader

+ +

+ Contains the header content of the dialog, including the title and description. +

+

+ Same props as a {"

"} element. +

+
+ +
+

AlertDialogFooter

+ +

+ Contains the footer content of the dialog, including the action and cancel buttons. +

+

+ Same props as a {"

"} element. +

+
+ +
+

AlertDialogTitle

+ +

An accessible name to be announced when the dialog is opened.

+

+ Alternatively, you can provide aria-label or{" "} + aria-labelledby to AlertDialogContent and exclude this + component. +

+

+ Radix{" "} + + AlertDialog.Title + {" "} + props. +

+
+ +
+

AlertDialogDescription

+ +

An accessible description to be announced when the dialog is opened.

+

+ Alternatively, you can provide aria-describedby to{" "} + AlertDialogContent and exclude this component. +

+

+ Radix{" "} + + AlertDialog.Description + {" "} + props. +

+
+ +
+

AlertDialogAction

+ +

+ A button that closes the alert dialog and confirms the action. Will default to{" "} + appearance="filled", as well as the priority color from the{" "} + AlertDialog +

+

+ These buttons should be distinguished visually from the AlertDialogCancel button. +

+

+ Composes around the mantle Button component. +

+

+ Same props as the Button component. +

+
+ +
+

AlertDialogCancel

+ +

+ A button that closes the dialog and cancels the action. Will default to{" "} + appearance="outlined" and priority="neutral". +

+

+ This button should be distinguished visually from AlertDialogAction buttons. +

+

+ Composes around the mantle Button component. +

+

+ Same props as the Button component. +

+
+
+
+ ); +} diff --git a/apps/www/app/types/routes.ts b/apps/www/app/types/routes.ts index 260ff55c..6ec9d87f 100644 --- a/apps/www/app/types/routes.ts +++ b/apps/www/app/types/routes.ts @@ -22,6 +22,7 @@ export const routePatterns = [ "/components/label", "/components/media-object", "/components/password-input", + "/components/preview/alert-dialog", "/components/preview/calendar", "/components/preview/pagination", "/components/preview/popover", @@ -69,6 +70,7 @@ export const routes = [ "/components/label", "/components/media-object", "/components/password-input", + "/components/preview/alert-dialog", "/components/preview/calendar", "/components/preview/pagination", "/components/preview/popover", diff --git a/packages/mantle/package.json b/packages/mantle/package.json index 250399d2..6c49844d 100644 --- a/packages/mantle/package.json +++ b/packages/mantle/package.json @@ -3,7 +3,7 @@ "description": "mantle is ngrok's UI library and design system.", "author": "ngrok", "license": "MIT", - "version": "0.12.1", + "version": "0.13.0", "homepage": "https://mantle.ngrok.com", "repository": { "type": "git", @@ -37,6 +37,7 @@ }, "dependencies": { "@headlessui/react": "2.2.0", + "@radix-ui/react-alert-dialog": "1.1.2", "@radix-ui/react-dialog": "1.1.2", "@radix-ui/react-dropdown-menu": "2.1.2", "@radix-ui/react-popover": "1.1.2", @@ -96,183 +97,187 @@ "./mantle.css": "./assets/mantle.css", "./package.json": "./package.json", "./tailwind-preset": { - "@ngrok/mantle/source": "./src/tailwind-preset/index.ts", + "@ngrok/mantle/source": "./src/tailwind-preset/index.ts", "types": "./dist/tailwind-preset.d.ts", "import": "./dist/tailwind-preset.js", "require": "./dist/tailwind-preset.cjs" }, "./alert": { - "@ngrok/mantle/source": "./src/components/alert/index.ts", + "@ngrok/mantle/source": "./src/components/alert/index.ts", "import": "./dist/alert.js", "types": "./dist/alert.d.ts" }, + "./alert-dialog": { + "@ngrok/mantle/source": "./src/components/alert-dialog/index.ts", + "import": "./dist/alert-dialog.js", + "types": "./dist/alert-dialog.d.ts" + }, "./anchor": { - "@ngrok/mantle/source": "./src/components/anchor/index.ts", + "@ngrok/mantle/source": "./src/components/anchor/index.ts", "import": "./dist/anchor.js", "types": "./dist/anchor.d.ts" }, "./badge": { - "@ngrok/mantle/source": "./src/components/badge/index.ts", + "@ngrok/mantle/source": "./src/components/badge/index.ts", "import": "./dist/badge.js", "types": "./dist/badge.d.ts" }, "./button": { - "@ngrok/mantle/source": "./src/components/button/index.ts", + "@ngrok/mantle/source": "./src/components/button/index.ts", "import": "./dist/button.js", "types": "./dist/button.d.ts" }, "./calendar": { - "@ngrok/mantle/source": "./src/components/calendar/index.ts", + "@ngrok/mantle/source": "./src/components/calendar/index.ts", "import": "./dist/calendar.js", "types": "./dist/calendar.d.ts" }, "./card": { - "@ngrok/mantle/source": "./src/components/card/index.ts", + "@ngrok/mantle/source": "./src/components/card/index.ts", "import": "./dist/card.js", "types": "./dist/card.d.ts" }, "./checkbox": { - "@ngrok/mantle/source": "./src/components/checkbox/index.ts", + "@ngrok/mantle/source": "./src/components/checkbox/index.ts", "import": "./dist/checkbox.js", "types": "./dist/checkbox.d.ts" }, "./code-block": { - "@ngrok/mantle/source": "./src/components/code-block/index.ts", + "@ngrok/mantle/source": "./src/components/code-block/index.ts", "import": "./dist/code-block.js", "types": "./dist/code-block.d.ts" }, "./color": { - "@ngrok/mantle/source": "./src/utils/color/index.ts", + "@ngrok/mantle/source": "./src/utils/color/index.ts", "import": "./dist/color.js", "types": "./dist/color.d.ts" }, "./compose-refs": { - "@ngrok/mantle/source": "./src/utils/compose-refs/index.ts", + "@ngrok/mantle/source": "./src/utils/compose-refs/index.ts", "import": "./dist/compose-refs.js", "types": "./dist/compose-refs.d.ts" }, "./cx": { - "@ngrok/mantle/source": "./src/utils/cx/index.ts", + "@ngrok/mantle/source": "./src/utils/cx/index.ts", "import": "./dist/cx.js", "types": "./dist/cx.d.ts" }, - "./data-table": { - "@ngrok/mantle/source": "./src/components/data-table/index.ts" - }, - "./date-picker": { - }, + "./data-table": { + "@ngrok/mantle/source": "./src/components/data-table/index.ts" + }, + "./date-picker": {}, "./dialog": { - "@ngrok/mantle/source": "./src/components/dialog/index.ts", + "@ngrok/mantle/source": "./src/components/dialog/index.ts", "import": "./dist/dialog.js", "types": "./dist/dialog.d.ts" }, "./dropdown-menu": { - "@ngrok/mantle/source": "./src/components/dropdown-menu/index.ts", + "@ngrok/mantle/source": "./src/components/dropdown-menu/index.ts", "import": "./dist/dropdown-menu.js", "types": "./dist/dropdown-menu.d.ts" }, "./hooks": { - "@ngrok/mantle/source": "./src/hooks/index.ts", + "@ngrok/mantle/source": "./src/hooks/index.ts", "import": "./dist/hooks.js", "types": "./dist/hooks.d.ts" }, "./icon": { - "@ngrok/mantle/source": "./src/components/icon/index.ts", + "@ngrok/mantle/source": "./src/components/icon/index.ts", "import": "./dist/icon.js", "types": "./dist/icon.d.ts" }, "./inline-code": { - "@ngrok/mantle/source": "./src/components/inline-code/index.ts", + "@ngrok/mantle/source": "./src/components/inline-code/index.ts", "import": "./dist/inline-code.js", "types": "./dist/inline-code.d.ts" }, "./input": { - "@ngrok/mantle/source": "./src/components/input/index.ts", + "@ngrok/mantle/source": "./src/components/input/index.ts", "import": "./dist/input.js", "types": "./dist/input.d.ts" }, "./label": { - "@ngrok/mantle/source": "./src/components/label/index.ts", + "@ngrok/mantle/source": "./src/components/label/index.ts", "import": "./dist/label.js", "types": "./dist/label.d.ts" }, "./media-object": { - "@ngrok/mantle/source": "./src/components/media-object/index.ts", + "@ngrok/mantle/source": "./src/components/media-object/index.ts", "import": "./dist/media-object.js", "types": "./dist/media-object.d.ts" }, "./pagination": { - "@ngrok/mantle/source": "./src/components/pagination/index.ts", + "@ngrok/mantle/source": "./src/components/pagination/index.ts", "import": "./dist/pagination.js", "types": "./dist/pagination.d.ts" }, "./popover": { - "@ngrok/mantle/source": "./src/components/popover/index.ts", + "@ngrok/mantle/source": "./src/components/popover/index.ts", "import": "./dist/popover.js", "types": "./dist/popover.d.ts" }, "./progress": { - "@ngrok/mantle/source": "./src/components/progress/index.ts", + "@ngrok/mantle/source": "./src/components/progress/index.ts", "import": "./dist/progress.js", "types": "./dist/progress.d.ts" }, "./radio-group": { - "@ngrok/mantle/source": "./src/components/radio-group/index.ts", + "@ngrok/mantle/source": "./src/components/radio-group/index.ts", "import": "./dist/radio-group.js", "types": "./dist/radio-group.d.ts" }, "./select": { - "@ngrok/mantle/source": "./src/components/select/index.ts", + "@ngrok/mantle/source": "./src/components/select/index.ts", "import": "./dist/select.js", "types": "./dist/select.d.ts" }, "./separator": { - "@ngrok/mantle/source": "./src/components/separator/index.ts", + "@ngrok/mantle/source": "./src/components/separator/index.ts", "import": "./dist/separator.js", "types": "./dist/separator.d.ts" }, "./sheet": { - "@ngrok/mantle/source": "./src/components/sheet/index.ts", + "@ngrok/mantle/source": "./src/components/sheet/index.ts", "import": "./dist/sheet.js", "types": "./dist/sheet.d.ts" }, "./skeleton": { - "@ngrok/mantle/source": "./src/components/skeleton/index.ts", + "@ngrok/mantle/source": "./src/components/skeleton/index.ts", "import": "./dist/skeleton.js", "types": "./dist/skeleton.d.ts" }, "./switch": { - "@ngrok/mantle/source": "./src/components/switch/index.ts", + "@ngrok/mantle/source": "./src/components/switch/index.ts", "import": "./dist/switch.js", "types": "./dist/switch.d.ts" }, "./table": { - "@ngrok/mantle/source": "./src/components/table/index.ts", + "@ngrok/mantle/source": "./src/components/table/index.ts", "import": "./dist/table.js", "types": "./dist/table.d.ts" }, "./tabs": { - "@ngrok/mantle/source": "./src/components/tabs/index.ts", + "@ngrok/mantle/source": "./src/components/tabs/index.ts", "import": "./dist/tabs.js", "types": "./dist/tabs.d.ts" }, "./text-area": { - "@ngrok/mantle/source": "./src/components/text-area/index.ts", + "@ngrok/mantle/source": "./src/components/text-area/index.ts", "import": "./dist/text-area.js", "types": "./dist/text-area.d.ts" }, "./theme-provider": { - "@ngrok/mantle/source": "./src/components/theme-provider/index.ts", + "@ngrok/mantle/source": "./src/components/theme-provider/index.ts", "import": "./dist/theme-provider.js", "types": "./dist/theme-provider.d.ts" }, "./tooltip": { - "@ngrok/mantle/source": "./src/components/tooltip/index.ts", + "@ngrok/mantle/source": "./src/components/tooltip/index.ts", "import": "./dist/tooltip.js", "types": "./dist/tooltip.d.ts" }, "./types": { - "@ngrok/mantle/source": "./src/types/index.ts", + "@ngrok/mantle/source": "./src/types/index.ts", "types": "./dist/types.d.ts" } } diff --git a/packages/mantle/src/components/alert-dialog/alert-dialog.tsx b/packages/mantle/src/components/alert-dialog/alert-dialog.tsx new file mode 100644 index 00000000..818b757f --- /dev/null +++ b/packages/mantle/src/components/alert-dialog/alert-dialog.tsx @@ -0,0 +1,279 @@ +"use client"; + +import { Info } from "@phosphor-icons/react/Info"; +import { Warning } from "@phosphor-icons/react/Warning"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; +import { + createContext, + forwardRef, + useContext, + type ComponentProps, + type ComponentPropsWithoutRef, + type ElementRef, + type ReactNode, +} from "react"; +import invariant from "tiny-invariant"; +import { cx } from "../../utils/cx/cx.js"; +import { Button, type ButtonPriority, type ButtonProps } from "../button/button.js"; +import { IconBase } from "../icon/_icon-base.js"; +import type { SvgAttributes } from "../icon/types.js"; + +const priorities = ["info", "danger"] as const; +type Priority = (typeof priorities)[number]; + +type AlertDialogContextValue = { + priority: Priority; +}; + +const AlertDialogContext = createContext(null); + +function useAlertDialogContext() { + const context = useContext(AlertDialogContext); + invariant(context, "AlertDialog child component used outside of AlertDialog parent!"); + return context; +} + +type AlertDialogProps = ComponentProps & { + /** + * Indicates the importance or impact level of the AlertDialog, affecting its + * color and styling to communicate its purpose to the user. + */ + priority: Priority; +}; + +/** + * The root component for the Alert Dialog + */ +function AlertDialog({ priority, ...props }: AlertDialogProps) { + return ( + + + + ); +} + +/** + * A button that opens the Alert Dialog. + */ +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +/** + * A layer that covers the inert portion of the view when the dialog is open. + */ +const AlertDialogOverlay = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +/** + * The popover alert dialog container. + * + * Renders on top of the overlay and is centered in the viewport. + */ +const AlertDialogContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + +
+ +
+
+)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +/** + * Contains the main content of the dialog. + */ +const AlertDialogBody = ({ className, ...props }: ComponentProps<"div">) => ( +
+); + +/** + * Contains the header content of the dialog, including the title and description. + */ +const AlertDialogHeader = ({ className, ...props }: ComponentProps<"div">) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +/** + * Contains the footer content of the dialog, including the action and cancel buttons. + */ +const AlertDialogFooter = ({ className, ...props }: ComponentProps<"div">) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +/** + * An accessible name to be announced when the dialog is opened. + * + * Alternatively, you can provide `aria-label` or `aria-labelledby` to + * `AlertDialogContent` and exclude this component. + */ +const AlertDialogTitle = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +/** + * An accessible description to be announced when the dialog is opened. + * + * Alternatively, you can provide `aria-describedby` to `AlertDialogContent` and + * exclude this component. + */ +const AlertDialogDescription = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +/** + * A button that closes the dialog and confirms the action. + * Will default to appearance="filled", as well as the priority color from the `AlertDialog`. + * + * These buttons should be distinguished visually from the AlertDialogCancel button. + * + * Composes around the mantle Button component. + */ +const AlertDialogAction = forwardRef, ButtonProps>( + ( + { + //, + appearance = "filled", + ...props + }, + ref, + ) => { + const ctx = useAlertDialogContext(); + let buttonPriority: NonNullable = "default"; + if (ctx.priority === "danger") { + buttonPriority = "danger"; + } + + return ( + +