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
7 changes: 5 additions & 2 deletions components/docs/interactive-examples/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ function AlertExampleContent() {
await alert({
title: "Heads up",
description: "This is an interactive alert demo.",
buttonLabel: "Understood",
})
setStatus("Alert dismissed.")
}
Expand All @@ -29,7 +28,11 @@ function AlertExampleContent() {

export function AlertDialogInteractiveExample() {
return (
<AlertDialogProvider>
<AlertDialogProvider
messages={{
buttonLabel: "Understood",
}}
>
<AlertExampleContent />
</AlertDialogProvider>
)
Expand Down
10 changes: 7 additions & 3 deletions components/docs/interactive-examples/confirm-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -35,7 +33,13 @@ function ConfirmExampleContent() {

export function ConfirmDialogInteractiveExample() {
return (
<ConfirmDialogProvider>
<ConfirmDialogProvider
messages={{
confirmButtonText: "Delete",
cancelButtonText: "Keep item",
requireConfirmationLabel: "Type {{confirmTerm}} to delete this item",
}}
>
<ConfirmExampleContent />
</ConfirmDialogProvider>
)
Expand Down
31 changes: 29 additions & 2 deletions content/docs/components/alert-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<AlertDialogProvider
messages={{
title: "Notice",
buttonLabel: "Understood",
}}
>
{children}
</AlertDialogProvider>
)
}
```

## Configuration

`AlertDialogProvider` accepts:

| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `messages` | `Partial<AlertDialogMessages>` | `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 |
Expand Down
89 changes: 83 additions & 6 deletions content/docs/components/confirm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,100 @@ 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 (
<ConfirmDialogProvider
defaultVariant="destructive"
messages={{
title: "Please confirm",
description: "Review this action before continuing.",
confirmButtonText: "Continue",
cancelButtonText: "Go back",
requireConfirmationLabel: "Type {{confirmTerm}} to continue",
}}
>
{children}
</ConfirmDialogProvider>
)
}
```

## 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 (
<AlertDialogProvider
messages={{
title: tAlert("title"),
buttonLabel: tAlert("buttonLabel"),
}}
>
<ConfirmDialogProvider
messages={{
title: tConfirm("title"),
description: tConfirm("description"),
confirmButtonText: tConfirm("confirmButtonText"),
cancelButtonText: tConfirm("cancelButtonText"),
requireConfirmationLabel: tConfirm("requireConfirmationLabel"),
}}
>
{children}
</ConfirmDialogProvider>
</AlertDialogProvider>
)
}
```

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<ConfirmDialogMessages>` | `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`:

| 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`.
25 changes: 22 additions & 3 deletions registry/radix-nova/alert-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export interface AlertOptions {
children?: ReactNode;
}

export interface AlertDialogMessages {
title: string;
buttonLabel: string;
}

export interface AlertDialogProviderProps {
children: ReactNode;
messages?: Partial<AlertDialogMessages>;
}

export interface AlertApi {
(options: AlertOptions): Promise<void>;
dismiss: () => void;
Expand All @@ -24,6 +34,11 @@ interface AlertContextType {

const AlertContext = createContext<AlertContextType | undefined>(undefined);

const DEFAULT_ALERT_MESSAGES: AlertDialogMessages = {
title: "Notice",
buttonLabel: "OK",
};

function AlertDialogContent_({
isOpen,
title,
Expand Down Expand Up @@ -67,10 +82,14 @@ function AlertDialogContent_({
);
}

export const AlertDialogProvider = ({ children }: { children: ReactNode }) => {
export const AlertDialogProvider = ({ children, messages }: AlertDialogProviderProps) => {
const [options, setOptions] = useState<AlertOptions>({});
const [isOpen, setIsOpen] = useState(false);
const resolverRef = useRef<(() => void) | null>(null);
const resolvedMessages = {
...DEFAULT_ALERT_MESSAGES,
...messages,
};

const resolvePending = useCallback(() => {
const resolver = resolverRef.current;
Expand Down Expand Up @@ -103,9 +122,9 @@ export const AlertDialogProvider = ({ children }: { children: ReactNode }) => {
{children}
<AlertDialogContent_
isOpen={isOpen}
title={options.title ?? "Notice"}
title={options.title ?? resolvedMessages.title}
description={options.description ?? ""}
buttonLabel={options.buttonLabel ?? "OK"}
buttonLabel={options.buttonLabel ?? resolvedMessages.buttonLabel}
dismissButtonClassName={options.dismissButtonClassName}
dismissButtonVariant={options.dismissButtonVariant}
onDismiss={handleDismiss}
Expand Down
3 changes: 2 additions & 1 deletion registry/radix-nova/confirm-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface ConfirmDialogProps {
variant: "default" | "destructive";
isOpen: boolean;
requireConfirmationInput?: RequireConfirmationInput;
requireConfirmationLabel: string;
}

const ConfirmDialog = (props: ConfirmDialogProps) => {
Expand All @@ -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 (
Expand Down
41 changes: 34 additions & 7 deletions registry/radix-nova/confirm-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ConfirmDialogMessages>;
defaultVariant?: ConfirmOptions["variant"];
}

interface ConfirmContextType {
confirm: (options: ConfirmOptions) => Promise<boolean>;
}

const ConfirmContext = createContext<ConfirmContextType | undefined>(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<ConfirmOptions>({});
const [isOpen, setIsOpen] = useState(false);
const [resolver, setResolver] = useState<((value: boolean) => void) | null>(null);
const resolvedMessages = {
...DEFAULT_CONFIRM_MESSAGES,
...messages,
};
Comment thread
phev8 marked this conversation as resolved.

const confirm = useCallback((confirmOptions: ConfirmOptions) => {
setResolver((prev: ((value: boolean) => void) | null) => {
Expand Down Expand Up @@ -61,12 +87,13 @@ export const ConfirmDialogProvider = ({ children }: { children: ReactNode }) =>
{children}
<ConfirmDialog
isOpen={isOpen}
title={options.title || "Confirm Action"}
description={options.description || "Are you sure you want to proceed?"}
confirmButtonText={options.confirmButtonText || "Confirm"}
cancelButtonText={options.cancelButtonText || "Cancel"}
variant={options.variant || "default"}
title={options.title ?? resolvedMessages.title}
description={options.description ?? resolvedMessages.description}
confirmButtonText={options.confirmButtonText ?? resolvedMessages.confirmButtonText}
cancelButtonText={options.cancelButtonText ?? resolvedMessages.cancelButtonText}
variant={options.variant ?? defaultVariant}
requireConfirmationInput={options.requireConfirmationInput}
requireConfirmationLabel={resolvedMessages.requireConfirmationLabel}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
Expand Down
Loading