From c465dc2d6011df349b6007388ed1b04256c31685 Mon Sep 17 00:00:00 2001 From: phev8 Date: Thu, 30 Apr 2026 10:01:49 +0200 Subject: [PATCH 1/2] feat: enhance AlertDialog and ConfirmDialog providers with customizable message defaults --- .../interactive-examples/alert-dialog.tsx | 7 +- .../interactive-examples/confirm-dialog.tsx | 10 ++- content/docs/components/alert-provider.mdx | 31 ++++++- content/docs/components/confirm.mdx | 87 +++++++++++++++++-- registry/radix-nova/alert-provider.tsx | 25 +++++- registry/radix-nova/confirm-dialog.tsx | 9 +- registry/radix-nova/confirm-provider.tsx | 41 +++++++-- 7 files changed, 185 insertions(+), 25 deletions(-) diff --git a/components/docs/interactive-examples/alert-dialog.tsx b/components/docs/interactive-examples/alert-dialog.tsx index 374aedb..f6d0942 100644 --- a/components/docs/interactive-examples/alert-dialog.tsx +++ b/components/docs/interactive-examples/alert-dialog.tsx @@ -12,7 +12,6 @@ function AlertExampleContent() { await alert({ title: "Heads up", description: "This is an interactive alert demo.", - buttonLabel: "Understood", }) setStatus("Alert dismissed.") } @@ -29,7 +28,11 @@ function AlertExampleContent() { export function AlertDialogInteractiveExample() { return ( - + ) diff --git a/components/docs/interactive-examples/confirm-dialog.tsx b/components/docs/interactive-examples/confirm-dialog.tsx index d50e2e7..3f04427 100644 --- a/components/docs/interactive-examples/confirm-dialog.tsx +++ b/components/docs/interactive-examples/confirm-dialog.tsx @@ -12,8 +12,6 @@ function ConfirmExampleContent() { const isConfirmed = await confirm({ title: "Delete item?", description: "This action cannot be undone.", - confirmButtonText: "Delete", - cancelButtonText: "Cancel", variant: "destructive", requireConfirmationInput: { confirmTerm: "DELETE", @@ -35,7 +33,13 @@ function ConfirmExampleContent() { export function ConfirmDialogInteractiveExample() { return ( - + `Type ${confirmTerm} to delete this item`, + }} + > ) diff --git a/content/docs/components/alert-provider.mdx b/content/docs/components/alert-provider.mdx index 5a5abc0..4c78d6f 100644 --- a/content/docs/components/alert-provider.mdx +++ b/content/docs/components/alert-provider.mdx @@ -54,15 +54,42 @@ export function AlertExample() { } ``` +## Provider Defaults + +`AlertDialogProvider` can define app-level copy defaults. Per-call values passed to `alert()` still win. + +```tsx +import { AlertDialogProvider } from "@/components/c-ui/alert-provider" + +export function AppProviders({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} +``` + ## Configuration +`AlertDialogProvider` accepts: + +| Prop | Type | Default | Notes | +| --- | --- | --- | --- | +| `messages` | `Partial` | `undefined` | App-level copy defaults | + `useAlert()` accepts these options: | Option | Type | Default | Notes | | --- | --- | --- | --- | -| `title` | `string` | `"Notice"` | Dialog title | +| `title` | `string` | Provider default or `"Notice"` | Dialog title | | `description` | `string` | `""` | Supporting text under title | -| `buttonLabel` | `string` | `"OK"` | Dismiss button label | +| `buttonLabel` | `string` | Provider default or `"OK"` | Dismiss button label | | `dismissButtonVariant` | `AlertDialogCancel["variant"]` | `"default"` | Button style variant | | `dismissButtonClassName` | `string` | `undefined` | Extra class names for dismiss button | | `children` | `ReactNode` | `undefined` | Optional custom content between description and footer | diff --git a/content/docs/components/confirm.mdx b/content/docs/components/confirm.mdx index 6c0e356..76ca031 100644 --- a/content/docs/components/confirm.mdx +++ b/content/docs/components/confirm.mdx @@ -60,17 +60,92 @@ export function ConfirmExample() { } ``` +## Provider Defaults + +`ConfirmDialogProvider` can define app-level defaults for copy and variant. Per-call values passed to `confirm()` still win. + +```tsx +import { ConfirmDialogProvider } from "@/components/c-ui/confirm-provider" + +export function AppProviders({ children }: { children: React.ReactNode }) { + return ( + + `Type ${confirmTerm} to continue`, + }} + > + {children} + + ) +} +``` + +## Next.js + next-intl + +Keep translation hooks in your app and pass the resolved strings into the provider: + +```tsx +"use client" + +import type { ReactNode } from "react" +import { useTranslations } from "next-intl" +import { AlertDialogProvider } from "@/components/c-ui/alert-provider" +import { ConfirmDialogProvider } from "@/components/c-ui/confirm-provider" + +export function AppDialogProviders({ children }: { children: ReactNode }) { + const tConfirm = useTranslations("dialogs.confirm") + const tAlert = useTranslations("dialogs.alert") + + return ( + + + tConfirm("requireConfirmationLabel", { confirmTerm }), + }} + > + {children} + + + ) +} +``` + +Then place `AppDialogProviders` near the top of your app tree, for example in `app/providers.tsx` or `app/layout.tsx`. + ## Configuration +`ConfirmDialogProvider` accepts: + +| Prop | Type | Default | Notes | +| --- | --- | --- | --- | +| `messages` | `Partial` | `undefined` | App-level copy defaults | +| `defaultVariant` | `"default" \| "destructive"` | `"default"` | Default confirm button variant | + `useConfirm()` accepts these options: | Option | Type | Default | Notes | | --- | --- | --- | --- | -| `title` | `string` | `"Confirm Action"` | Dialog title | -| `description` | `string` | `"Are you sure you want to proceed?"` | Description text | -| `confirmButtonText` | `string` | `"Confirm"` | Confirm action label | -| `cancelButtonText` | `string` | `"Cancel"` | Cancel action label | -| `variant` | `"default" \| "destructive"` | `"default"` | Confirm button style | +| `title` | `string` | Provider default or `"Confirm Action"` | Dialog title | +| `description` | `string` | Provider default or `"Are you sure you want to proceed?"` | Description text | +| `confirmButtonText` | `string` | Provider default or `"Confirm"` | Confirm action label | +| `cancelButtonText` | `string` | Provider default or `"Cancel"` | Cancel action label | +| `variant` | `"default" \| "destructive"` | Provider `defaultVariant` or `"default"` | Confirm button style | | `requireConfirmationInput` | `RequireConfirmationInput` | `undefined` | Forces typed confirmation | `RequireConfirmationInput`: @@ -78,5 +153,5 @@ export function ConfirmExample() { | Field | Type | Required | Notes | | --- | --- | --- | --- | | `confirmTerm` | `string` | Yes | Exact term user must type | -| `label` | `string` | No | Supports `{{confirmTerm}}` placeholder | +| `label` | `string` | No | Per-call label. Supports `{{confirmTerm}}` placeholder. | | `hint` | `string` | No | Helper text below input | diff --git a/registry/radix-nova/alert-provider.tsx b/registry/radix-nova/alert-provider.tsx index 3052c41..a148476 100644 --- a/registry/radix-nova/alert-provider.tsx +++ b/registry/radix-nova/alert-provider.tsx @@ -13,6 +13,16 @@ export interface AlertOptions { children?: ReactNode; } +export interface AlertDialogMessages { + title: string; + buttonLabel: string; +} + +export interface AlertDialogProviderProps { + children: ReactNode; + messages?: Partial; +} + export interface AlertApi { (options: AlertOptions): Promise; dismiss: () => void; @@ -24,6 +34,11 @@ interface AlertContextType { const AlertContext = createContext(undefined); +const DEFAULT_ALERT_MESSAGES: AlertDialogMessages = { + title: "Notice", + buttonLabel: "OK", +}; + function AlertDialogContent_({ isOpen, title, @@ -67,10 +82,14 @@ function AlertDialogContent_({ ); } -export const AlertDialogProvider = ({ children }: { children: ReactNode }) => { +export const AlertDialogProvider = ({ children, messages }: AlertDialogProviderProps) => { const [options, setOptions] = useState({}); const [isOpen, setIsOpen] = useState(false); const resolverRef = useRef<(() => void) | null>(null); + const resolvedMessages = { + ...DEFAULT_ALERT_MESSAGES, + ...messages, + }; const resolvePending = useCallback(() => { const resolver = resolverRef.current; @@ -103,9 +122,9 @@ export const AlertDialogProvider = ({ children }: { children: ReactNode }) => { {children} string; } const ConfirmDialog = (props: ConfirmDialogProps) => { @@ -31,8 +32,12 @@ const ConfirmDialog = (props: ConfirmDialogProps) => { const requiredTerm = requireTyped?.confirmTerm ?? ""; const canConfirm = !requireTyped || typedValue === requiredTerm; - const labelMessage = requireTyped?.label ?? "Type {{confirmTerm}} to confirm"; - const labelParts = labelMessage.split("{{confirmTerm}}"); + const labelMessage = requireTyped?.label ?? props.getRequireConfirmationLabel(requiredTerm); + const labelParts = labelMessage.includes("{{confirmTerm}}") + ? labelMessage.split("{{confirmTerm}}") + : requiredTerm && labelMessage.includes(requiredTerm) + ? labelMessage.split(requiredTerm) + : [labelMessage]; return ( string; +} + +export interface ConfirmDialogProviderProps { + children: ReactNode; + messages?: Partial; + defaultVariant?: ConfirmOptions["variant"]; +} + interface ConfirmContextType { confirm: (options: ConfirmOptions) => Promise; } const ConfirmContext = createContext(undefined); -export const ConfirmDialogProvider = ({ children }: { children: ReactNode }) => { +const DEFAULT_CONFIRM_MESSAGES: ConfirmDialogMessages = { + title: "Confirm Action", + description: "Are you sure you want to proceed?", + confirmButtonText: "Confirm", + cancelButtonText: "Cancel", + getRequireConfirmationLabel: (confirmTerm) => `Type ${confirmTerm} to confirm`, +}; + +export const ConfirmDialogProvider = ({ children, messages, defaultVariant = "default" }: ConfirmDialogProviderProps) => { const [options, setOptions] = useState({}); const [isOpen, setIsOpen] = useState(false); const [resolver, setResolver] = useState<((value: boolean) => void) | null>(null); + const resolvedMessages = { + ...DEFAULT_CONFIRM_MESSAGES, + ...messages, + }; const confirm = useCallback((confirmOptions: ConfirmOptions) => { setResolver((prev: ((value: boolean) => void) | null) => { @@ -61,12 +87,13 @@ export const ConfirmDialogProvider = ({ children }: { children: ReactNode }) => {children} From 1dc65a88d21f3d9ed7a962498c7733360a02fdeb Mon Sep 17 00:00:00 2001 From: phev8 Date: Thu, 30 Apr 2026 10:21:11 +0200 Subject: [PATCH 2/2] refactor: replace getRequireConfirmationLabel with requireConfirmationLabel for consistent message handling in ConfirmDialog --- .../docs/interactive-examples/confirm-dialog.tsx | 2 +- content/docs/components/confirm.mdx | 10 ++++++---- registry/radix-nova/confirm-dialog.tsx | 10 +++------- registry/radix-nova/confirm-provider.tsx | 6 +++--- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/components/docs/interactive-examples/confirm-dialog.tsx b/components/docs/interactive-examples/confirm-dialog.tsx index 3f04427..c9d331b 100644 --- a/components/docs/interactive-examples/confirm-dialog.tsx +++ b/components/docs/interactive-examples/confirm-dialog.tsx @@ -37,7 +37,7 @@ export function ConfirmDialogInteractiveExample() { messages={{ confirmButtonText: "Delete", cancelButtonText: "Keep item", - getRequireConfirmationLabel: (confirmTerm) => `Type ${confirmTerm} to delete this item`, + requireConfirmationLabel: "Type {{confirmTerm}} to delete this item", }} > diff --git a/content/docs/components/confirm.mdx b/content/docs/components/confirm.mdx index 76ca031..6aaf853 100644 --- a/content/docs/components/confirm.mdx +++ b/content/docs/components/confirm.mdx @@ -76,8 +76,7 @@ export function AppProviders({ children }: { children: React.ReactNode }) { description: "Review this action before continuing.", confirmButtonText: "Continue", cancelButtonText: "Go back", - getRequireConfirmationLabel: (confirmTerm) => - `Type ${confirmTerm} to continue`, + requireConfirmationLabel: "Type {{confirmTerm}} to continue", }} > {children} @@ -115,8 +114,7 @@ export function AppDialogProviders({ children }: { children: ReactNode }) { description: tConfirm("description"), confirmButtonText: tConfirm("confirmButtonText"), cancelButtonText: tConfirm("cancelButtonText"), - getRequireConfirmationLabel: (confirmTerm) => - tConfirm("requireConfirmationLabel", { confirmTerm }), + requireConfirmationLabel: tConfirm("requireConfirmationLabel"), }} > {children} @@ -128,6 +126,8 @@ export function AppDialogProviders({ children }: { children: ReactNode }) { Then place `AppDialogProviders` near the top of your app tree, for example in `app/providers.tsx` or `app/layout.tsx`. +The `dialogs.confirm.requireConfirmationLabel` message should keep the `{{confirmTerm}}` placeholder, for example `"Type {{confirmTerm}} to confirm"`. + ## Configuration `ConfirmDialogProvider` accepts: @@ -155,3 +155,5 @@ Then place `AppDialogProviders` near the top of your app tree, for example in `a | `confirmTerm` | `string` | Yes | Exact term user must type | | `label` | `string` | No | Per-call label. Supports `{{confirmTerm}}` placeholder. | | `hint` | `string` | No | Helper text below input | + +`ConfirmDialogProvider.messages.requireConfirmationLabel` uses the same `{{confirmTerm}}` placeholder as `RequireConfirmationInput.label`. diff --git a/registry/radix-nova/confirm-dialog.tsx b/registry/radix-nova/confirm-dialog.tsx index d7a7b92..1ba75f6 100644 --- a/registry/radix-nova/confirm-dialog.tsx +++ b/registry/radix-nova/confirm-dialog.tsx @@ -22,7 +22,7 @@ interface ConfirmDialogProps { variant: "default" | "destructive"; isOpen: boolean; requireConfirmationInput?: RequireConfirmationInput; - getRequireConfirmationLabel: (confirmTerm: string) => string; + requireConfirmationLabel: string; } const ConfirmDialog = (props: ConfirmDialogProps) => { @@ -32,12 +32,8 @@ const ConfirmDialog = (props: ConfirmDialogProps) => { const requiredTerm = requireTyped?.confirmTerm ?? ""; const canConfirm = !requireTyped || typedValue === requiredTerm; - const labelMessage = requireTyped?.label ?? props.getRequireConfirmationLabel(requiredTerm); - const labelParts = labelMessage.includes("{{confirmTerm}}") - ? labelMessage.split("{{confirmTerm}}") - : requiredTerm && labelMessage.includes(requiredTerm) - ? labelMessage.split(requiredTerm) - : [labelMessage]; + const labelMessage = requireTyped?.label ?? props.requireConfirmationLabel; + const labelParts = labelMessage.split("{{confirmTerm}}"); return ( string; + requireConfirmationLabel: string; } export interface ConfirmDialogProviderProps { @@ -39,7 +39,7 @@ const DEFAULT_CONFIRM_MESSAGES: ConfirmDialogMessages = { description: "Are you sure you want to proceed?", confirmButtonText: "Confirm", cancelButtonText: "Cancel", - getRequireConfirmationLabel: (confirmTerm) => `Type ${confirmTerm} to confirm`, + requireConfirmationLabel: "Type {{confirmTerm}} to confirm", }; export const ConfirmDialogProvider = ({ children, messages, defaultVariant = "default" }: ConfirmDialogProviderProps) => { @@ -93,7 +93,7 @@ export const ConfirmDialogProvider = ({ children, messages, defaultVariant = "de cancelButtonText={options.cancelButtonText ?? resolvedMessages.cancelButtonText} variant={options.variant ?? defaultVariant} requireConfirmationInput={options.requireConfirmationInput} - getRequireConfirmationLabel={resolvedMessages.getRequireConfirmationLabel} + requireConfirmationLabel={resolvedMessages.requireConfirmationLabel} onConfirm={handleConfirm} onCancel={handleCancel} />