Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Dialogs

Modal dialogs and prompts

Always use the usePrompt() hook for modal dialogs and confirmations. Do NOT use raw Dialog or Drawer components directly.

Basic Usage

import { usePrompt } from "@/forms/prompt";

function MyComponent() {
  const prompt = usePrompt();

  const handleDelete = () => {
    prompt({
      title: "Delete Item",
      description: "Are you sure you want to delete this item?",
      action: async () => {
        return deleteItem(id);
      },
      buttonProps: {
        variant: "destructive",
        icon: "trash",
        i18nButtonKey: "delete",
      },
    });
  };

  return <Button onClick={handleDelete}>Delete</Button>;
}

Auto-Close Behavior

The prompt automatically closes when the action callback returns { success: true } (matching the ActionResponse type).

const handleApprove = () => {
  prompt({
    action: async () => {
      // Must await and return the mutation result
      return await updateStatusMutation.mutateAsync({
        id: requestId,
        status: "active",
      });
    },
    title: t("approve.title"),
    buttonProps: {
      variant: "primary",
      icon: "check",
      i18nButtonKey: "approve",
    },
  });
};

Important

The action must return the result. The prompt checks for { success: true } to know when to close.

Prompt Options

OptionTypeDescription
titleReactNodeDialog title
descriptionReactNodeDialog description
iconIconKeysIcon to show in title
actionFunctionAsync function to execute on confirm
buttonPropsObjectProps for confirm button (variant, icon, i18nButtonKey)
childrenReactNodeCustom content for dialog body
formUseFormReturnreact-hook-form instance for form dialogs
customActionsFunctionCustom action buttons renderer

With Form Content

const handleChangeRole = (member: Member) => {
  prompt({
    title: t("members.actions.changeRole"),
    action: async () => {
      return updateMemberRole({
        memberId: member.id,
        role: selectedRoleRef.current,
      });
    },
    children: (
      <Select
        defaultValue={member.role}
        onValueChange={(value) => {
          selectedRoleRef.current = value;
        }}
      >
        <SelectTrigger>
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          {roles.map((role) => (
            <SelectItem key={role} value={role}>
              {t(`roles.${role}`)}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    ),
    buttonProps: {
      variant: "primary",
      icon: "edit",
      i18nButtonKey: "update",
    },
  });
};

With react-hook-form

const { form, execute } = useAuthDeleteAccount();
const prompt = usePrompt();

const handlePromptDelete = () => {
  prompt({
    title: t("confirmTitle"),
    description: t("confirmDescription"),
    action: execute,
    form: form,
    buttonProps: {
      variant: "destructive",
      icon: "trash",
      i18nButtonKey: "delete",
    },
    children: (
      <FormField
        control={form.control}
        name="confirmation"
        render={({ field }) => (
          <FormItem>
            <FormLabel>{t("confirmation.label")}</FormLabel>
            <FormControl>
              <Input {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />
    ),
  });
};

Form Re-rendering

Critical

When using form fields inside prompt children, use useWatch instead of form.watch(). The prompt content is rendered outside the normal component tree, so form.watch() won't trigger re-renders correctly.

// ❌ Won't re-render in prompt children
const value = form.watch("fieldName");

// ✅ Will re-render correctly
import { useWatch } from "react-hook-form";
const value = useWatch({ control: form.control, name: "fieldName" });

Custom Actions

For complex dialogs with multiple actions:

prompt({
  title: "Choose Action",
  customActions: (close) => (
    <div className="flex gap-2">
      <Button variant="outline" onClick={close}>
        Cancel
      </Button>
      <Button variant="secondary" onClick={() => handleSave(false)}>
        Save Draft
      </Button>
      <Button variant="primary" onClick={() => handleSave(true)}>
        Publish
      </Button>
    </div>
  ),
  children: <MyFormContent />,
});

Best Practices

Never use raw Dialog/Drawer components for confirmations

The action must return { success: true } for auto-close

Match button variant to action severity

For proper re-rendering inside prompt children

On this page

Basic Usage
Auto-Close Behavior
Prompt Options
With Form Content
With react-hook-form
Form Re-rendering
Custom Actions
Best Practices