You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Use the `useForm` helper for forms — follow existing patterns in the codebase.
13
13
- Use `<Link>` or `router.visit()` for navigation — never raw `<a>` tags for internal routes.
14
14
15
+
## Dialogs (centralized registry)
16
+
17
+
App-level dialogs are **not** mounted inline next to their trigger. They live in a central registry and are opened imperatively. This is the required pattern for any new modal/sheet — do not hand-roll local `open` state with a `<DialogTrigger>`.
18
+
19
+
**The pieces:**
20
+
-`resources/js/components/dialogs/registry.ts` — maps a typed key to a dialog component.
21
+
-`resources/js/hooks/use-dialog.ts` — `useDialog()` returns `dialog.<key>.open(props)` / `.close()` with full prop typing.
22
+
-`resources/js/stores/dialog-store.ts` — Zustand store holding the single active dialog.
23
+
-`resources/js/components/dialogs/dialog-host.tsx` — renders the active dialog once, app-wide.
**Opening from a dropdown — this is the whole point of the pattern:** use a plain `DropdownMenuItem` with the default `onSelect` so the menu closes, then open the dialog. **Never** wrap a `<Dialog>`/`<DialogTrigger>` inside a `DropdownMenuItem` with `onSelect={(e) => e.preventDefault()}` — that leaves the dropdown stuck open behind the dialog.
**Authoring a registered dialog component** — it takes control props, renders `<Dialog>` directly (NO `DialogTrigger`, NO local `open` state), and suppresses Radix close-autofocus (the store restores focus):
Then register it once in `registry.ts` (`firewallForm: FirewallRuleForm`). The consumer immediately gets `dialog.firewallForm.open(props)` with typed props.
79
+
80
+
**Where the dialog component file lives:** a dialog belongs to the module it serves — author it in that module's `components/` folder (e.g. the SSL activation dialog is `pages/server-ssls/components/activate-dialog.tsx`, the PHP ini dialog is `pages/php/components/ini-dialog.tsx`), and import it into `registry.ts` via its `@/pages/...` path. The folder already namespaces the file, so keep the filename short (`activate-dialog`, not `activate-server-ssl-dialog`). Only genuinely cross-cutting, generic dialogs (`confirmation-dialog`, `log-viewer-dialog`) and the registry infrastructure itself (`registry.ts`, `dialog-host.tsx`) live under `resources/js/components/dialogs/`.
81
+
82
+
**Rules & gotchas:**
83
+
-**Form submit buttons** use `form="<form-id>" type="submit"` and the `onSubmit` handler calls `e.preventDefault()`. Do not wire submit via the button's `onClick` (a bare `onClick={submit}` with no `preventDefault` lets Enter fire a native form submission alongside the Inertia request).
84
+
-**Always call `onOpenChange(false)` in `onSuccess`** to close after a successful request.
85
+
-**Lifecycle:**`DialogHost` renders the component with `open` hard-coded `true` and **unmounts it on close** (it never re-renders with `open=false`). Each open is a fresh instance (keyed by `instanceId`), so `useForm` state resets automatically — don't add manual reset-on-open effects. But any `useEffect` that pushes state *outward* based on `open` (e.g. `useInputFocus`'s `setFocused`) **must return a cleanup**, because the component unmounts while `open` is still `true`:
86
+
```tsx
87
+
useEffect(() => {
88
+
setFocused(open);
89
+
return () =>setFocused(false); // required — unmount happens with open===true
90
+
}, [open, setFocused]);
91
+
```
92
+
-**Authorization:** the registry carries no authz. Every dialog's props must come from server-authorised sources (Inertia page props, API resources) — never URL params or other user-controlled input.
93
+
15
94
## Bootstrap Context (`useConfigs`)
16
95
17
96
- Shared catalogue data (provider lists, site types, GitHub App install state, public key text) is **not** in `page.props` — it lives in the Zustand `useBootstrapStore`, exposed via `useConfigs()` and `usePublicKeyText()` from `@/stores/bootstrap-store`.
Copy file name to clipboardExpand all lines: CLAUDE.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -67,6 +67,7 @@ Vito has a specific architecture. Match these patterns exactly:
67
67
68
68
- Inertia pages in `resources/js/pages/`. React components in `resources/js/components/`. Functional components + hooks.
69
69
- Forms: use the `useForm` helper, follow existing patterns. Navigation: `<Link>` or `router.visit()` — never raw `<a>` for internal routes.
70
+
-**Dialogs**: all modals/sheets use the centralized registry (`resources/js/components/dialogs/registry.ts`) opened via `useDialog()` — `dialog.<key>.open(props)`, or `dialog.confirm.open({...})` for simple confirms. Never nest `<Dialog>`/`<DialogTrigger>` inside a `DropdownMenuItem`. **Read the "Dialogs" section of `.github/instructions/frontend.instructions.md` before adding or changing any dialog.**
70
71
- Tailwind v4: `@import "tailwindcss"`, `@theme` for config, **`gap-*` over margins** for sibling spacing.
71
72
- Use Shadcn components and semantic tokens (`text-foreground`, `bg-background`, `text-muted-foreground`). Avoid hard-coded colors and custom CSS.
72
73
-**React hooks**: include ALL dependencies in `useEffect` / `useMemo` / `useCallback` arrays. Return cleanup for timers/subscriptions. Stale closures from missing deps are a recurring bug here.
0 commit comments