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..c9d331b 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 ( - + ) 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..6aaf853 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 ( + + {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 ( + + + {children} + + + ) +} +``` + +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: + +| 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,7 @@ 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 | + +`ConfirmDialogProvider.messages.requireConfirmationLabel` uses the same `{{confirmTerm}}` placeholder as `RequireConfirmationInput.label`. 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} { @@ -31,7 +32,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => { const requiredTerm = requireTyped?.confirmTerm ?? ""; const canConfirm = !requireTyped || typedValue === requiredTerm; - const labelMessage = requireTyped?.label ?? "Type {{confirmTerm}} to confirm"; + const labelMessage = requireTyped?.label ?? props.requireConfirmationLabel; const labelParts = labelMessage.split("{{confirmTerm}}"); return ( diff --git a/registry/radix-nova/confirm-provider.tsx b/registry/radix-nova/confirm-provider.tsx index 2ddd0ed..98bddbb 100644 --- a/registry/radix-nova/confirm-provider.tsx +++ b/registry/radix-nova/confirm-provider.tsx @@ -5,7 +5,7 @@ import ConfirmDialog, { type RequireConfirmationInput } from "./confirm-dialog"; export type { RequireConfirmationInput }; -interface ConfirmOptions { +export interface ConfirmOptions { title?: string; description?: string; confirmButtonText?: string; @@ -14,16 +14,42 @@ interface ConfirmOptions { requireConfirmationInput?: RequireConfirmationInput; } +export interface ConfirmDialogMessages { + title: string; + description: string; + confirmButtonText: string; + cancelButtonText: string; + requireConfirmationLabel: 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", + requireConfirmationLabel: "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}