Documentation
Documentation
Introduction

Getting Started

Getting started
Getting StartedInstallationQuick StartProject Structure

Configuration

Configuration
ConfigurationEnvironment ConfigurationEdge ConfigDatabaseAuth SecretStripeFirebaseStorageGoogle Maps And Cloud Service AccountOAuth ProvidersEmail DeliverySentryFeature Flags

Architecture

Architecture
Architecture OverviewTech StackoRPC MiddlewareDesign Principles

Patterns

Patterns
Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

Database
DatabaseSetupSchema DefinitionDatabase OperationsMigrationsCaching
Data Tables

API

oRPCProceduresRoutersoRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

Storage
StorageConfigurationUsageBuckets
Stripe Billing

Extra

Caching

Templates

Templates
Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate oRPC RouterAdd Translations

Development

Development
DevelopmentCommandsAI AgentsBest Practices
Pulling Updates

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