pejay-ui 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +26 -0
  2. package/bin/cli.js +45 -15
  3. package/package.json +77 -54
  4. package/registry/buttons.json +3 -2
  5. package/registry/dropdowns.json +3 -1
  6. package/registry/forms.json +51 -23
  7. package/registry/hotkeys.json +12 -0
  8. package/registry/overlays.json +18 -2
  9. package/registry/panels.json +21 -0
  10. package/registry/skeleton.json +20 -0
  11. package/registry/spinner.json +13 -0
  12. package/templates/button/Button.tsx +8 -7
  13. package/templates/button/README.md +81 -0
  14. package/templates/button/index.ts +1 -2
  15. package/templates/form/{checkbox-group.tsx → choices/checkbox-group.tsx} +1 -1
  16. package/templates/form/{checkbox.tsx → choices/checkbox.tsx} +1 -1
  17. package/templates/form/{radio-group.tsx → choices/radio-group.tsx} +1 -1
  18. package/templates/form/{radio.tsx → choices/radio.tsx} +1 -1
  19. package/templates/form/choices/readme.checkbox-group.md +27 -0
  20. package/templates/form/choices/readme.checkbox.md +26 -0
  21. package/templates/form/choices/readme.radio-group.md +26 -0
  22. package/templates/form/choices/readme.radio.md +24 -0
  23. package/templates/form/choices/readme.switch.md +26 -0
  24. package/templates/form/{switch.tsx → choices/switch.tsx} +1 -1
  25. package/templates/form/{file-input.tsx → file/file-input.tsx} +2 -2
  26. package/templates/form/file/readme.file-input.md +26 -0
  27. package/templates/form/index.ts +19 -22
  28. package/templates/form/{amount-input.tsx → numeric/amount-input.tsx} +2 -2
  29. package/templates/form/{number-input.tsx → numeric/number-input.tsx} +2 -2
  30. package/templates/form/{range-slider.tsx → numeric/range-slider.tsx} +1 -1
  31. package/templates/form/numeric/readme.amount-input.md +27 -0
  32. package/templates/form/numeric/readme.number-input.md +26 -0
  33. package/templates/form/numeric/readme.range-slider.md +27 -0
  34. package/templates/form/{date-picker.tsx → pickers/date-picker.tsx} +2 -2
  35. package/templates/form/{date-range-picker.tsx → pickers/date-range-picker.tsx} +2 -2
  36. package/templates/form/pickers/readme.date-picker.md +26 -0
  37. package/templates/form/pickers/readme.date-range-picker.md +25 -0
  38. package/templates/form/pickers/readme.time-picker.md +25 -0
  39. package/templates/form/pickers/readme.time-range-picker.md +25 -0
  40. package/templates/form/{time-picker.tsx → pickers/time-picker.tsx} +1 -1
  41. package/templates/form/{time-range-picker.tsx → pickers/time-range-picker.tsx} +1 -1
  42. package/templates/form/{input.tsx → text-inputs/input.tsx} +1 -1
  43. package/templates/form/{password-input.tsx → text-inputs/password-input.tsx} +1 -1
  44. package/templates/form/text-inputs/readme.email-input.md +24 -0
  45. package/templates/form/text-inputs/readme.input.md +28 -0
  46. package/templates/form/text-inputs/readme.password-input.md +24 -0
  47. package/templates/form/text-inputs/readme.phone-input.md +24 -0
  48. package/templates/form/text-inputs/readme.textarea.md +24 -0
  49. package/templates/form/text-inputs/readme.url-input.md +23 -0
  50. package/templates/form/{textarea.tsx → text-inputs/textarea.tsx} +1 -1
  51. package/templates/hotkeys/README.md +134 -0
  52. package/templates/hotkeys/components/HotkeyProvider.tsx +78 -0
  53. package/templates/hotkeys/components/HotkeysHelpModal.tsx +102 -0
  54. package/templates/hotkeys/core/key-matcher.ts +106 -0
  55. package/templates/hotkeys/core/registry.ts +39 -0
  56. package/templates/hotkeys/core/types.ts +15 -0
  57. package/templates/hotkeys/hooks/useHotkey.ts +43 -0
  58. package/templates/hotkeys/index.ts +6 -0
  59. package/templates/layouts/lv1/app-layout.tsx +1 -1
  60. package/templates/layouts/lv1/sidebar-menu.tsx +1 -1
  61. package/templates/notes/app-provider/app-provider-side-panel-modals-roadmap.md +606 -0
  62. package/templates/notes/app-provider/manual-open-close-side-panel-and-modal.md +913 -0
  63. package/templates/notes/app-provider/side-panel-card-hooks-and-complexity.md +578 -0
  64. package/templates/notes/under-dev/AppProvider.tsx +92 -0
  65. package/templates/notes/under-dev/app-context.ts +14 -0
  66. package/templates/notes/under-dev/card/base-card.tsx +35 -0
  67. package/templates/notes/under-dev/card/index.ts +4 -0
  68. package/templates/notes/under-dev/card/modal-card.tsx +88 -0
  69. package/templates/notes/under-dev/card/side-panel-card.tsx +127 -0
  70. package/templates/notes/under-dev/form-overlay-registry.ts +42 -0
  71. package/templates/notes/under-dev/keyboard-shortcuts-help.tsx +79 -0
  72. package/templates/notes/under-dev/keyboard-utils.ts +22 -0
  73. package/templates/notes/under-dev/overlay/backdrop.tsx +95 -0
  74. package/templates/notes/under-dev/overlay/index.ts +4 -0
  75. package/templates/notes/under-dev/overlay/modal.tsx +43 -0
  76. package/templates/notes/under-dev/overlay/side-panel.tsx +126 -0
  77. package/templates/notes/under-dev/overlay-close.ts +50 -0
  78. package/templates/notes/under-dev/page-shortcut-registry.ts +9 -0
  79. package/templates/notes/under-dev/unsaved-changes-notify.ts +11 -0
  80. package/templates/notes/under-dev/use-keyboard-shortcuts.tsx +110 -0
  81. package/templates/notes/under-dev/useFormDirty.ts +6 -0
  82. package/templates/notes/under-dev/useFormOverlayRegistration.ts +47 -0
  83. package/templates/notes/under-dev/useFormPanel.tsx +18 -0
  84. package/templates/notes/under-dev/useFormTabHandler.ts +22 -0
  85. package/templates/notes/under-dev/useHorizontalWheelScroll.ts +27 -0
  86. package/templates/notes/under-dev/useOverlay.ts +41 -0
  87. package/templates/overlays/index.ts +2 -1
  88. package/templates/overlays/portal/portal.tsx +26 -0
  89. package/templates/overlays/tooltip/readme.tooltip.md +26 -0
  90. package/templates/{button → overlays/tooltip}/tooltip.tsx +1 -1
  91. package/templates/panels/COMPONENTS.md +103 -0
  92. package/templates/panels/README.md +702 -0
  93. package/templates/panels/components/base-card.tsx +33 -0
  94. package/templates/panels/components/index.ts +8 -0
  95. package/templates/panels/components/modal/backdrop.tsx +88 -0
  96. package/templates/panels/components/modal/modal-card.tsx +139 -0
  97. package/templates/panels/components/modal/modal-raw.tsx +36 -0
  98. package/templates/panels/components/modal/modal.tsx +49 -0
  99. package/templates/panels/components/side-panel/side-panel-card.tsx +123 -0
  100. package/templates/panels/components/side-panel/side-panel-raw.tsx +25 -0
  101. package/templates/panels/components/side-panel/side-panel.tsx +135 -0
  102. package/templates/panels/core/PanelProvider.tsx +145 -0
  103. package/templates/panels/core/constants.ts +9 -0
  104. package/templates/panels/core/form-overlay-registry.ts +35 -0
  105. package/templates/panels/core/index.ts +6 -0
  106. package/templates/panels/core/overlay-close.ts +11 -0
  107. package/templates/panels/core/panel-context.ts +41 -0
  108. package/templates/panels/core/types.ts +41 -0
  109. package/templates/panels/hooks/index.ts +7 -0
  110. package/templates/panels/hooks/useFormDirty.ts +6 -0
  111. package/templates/panels/hooks/useFormOverlayRegistration.ts +92 -0
  112. package/templates/panels/hooks/useFormPanel.tsx +18 -0
  113. package/templates/panels/hooks/useFormTabHandler.ts +25 -0
  114. package/templates/panels/hooks/useHorizontalWheelScroll.ts +31 -0
  115. package/templates/panels/hooks/useModalForm.tsx +22 -0
  116. package/templates/panels/hooks/useOverlay.ts +65 -0
  117. package/templates/panels/index.ts +3 -0
  118. package/templates/panels/vendor-example/by-using-modal/VendorModalPage.tsx +47 -0
  119. package/templates/panels/vendor-example/by-using-modal/useVendorModalForm.tsx +19 -0
  120. package/templates/panels/vendor-example/by-using-modal/vendor-modal-form.tsx +112 -0
  121. package/templates/panels/vendor-example/by-using-modal/vendor-types.ts +29 -0
  122. package/templates/panels/vendor-example/by-using-sidepanel/VendorsPage.tsx +47 -0
  123. package/templates/panels/vendor-example/by-using-sidepanel/useVendorFormPanel.tsx +15 -0
  124. package/templates/panels/vendor-example/by-using-sidepanel/vendor-form.tsx +108 -0
  125. package/templates/panels/vendor-example/by-using-sidepanel/vendor-types.ts +29 -0
  126. package/templates/select-dropdown/README.md +62 -0
  127. package/templates/select-dropdown/multiselect-input.tsx +2 -2
  128. package/templates/select-dropdown/select-input.tsx +2 -2
  129. package/templates/skeleton/README.md +53 -0
  130. package/templates/skeleton/index.ts +2 -0
  131. package/templates/skeleton/skeleton.css +36 -0
  132. package/templates/skeleton/skeleton.tsx +40 -0
  133. package/templates/skeleton/types.ts +12 -0
  134. package/templates/spinner/README.md +51 -0
  135. package/templates/spinner/index.ts +1 -0
  136. package/templates/spinner/spinner.css +58 -0
  137. package/templates/spinner/spinner.tsx +263 -0
  138. package/templates/toast/container.tsx +2 -2
  139. package/templates/utilities/formater.dateTime.md +74 -0
  140. package/templates/utilities/formater.dateTime.ts +310 -0
  141. package/templates/utilities/formater.phoneNumber.md +32 -0
  142. package/templates/utilities/formater.phoneNumber.ts +143 -0
  143. package/templates/utilities/sanitize.md +23 -0
  144. package/templates/utilities/sanitize.ts +148 -0
  145. /package/templates/form/{email-input.tsx → text-inputs/email-input.tsx} +0 -0
  146. /package/templates/form/{phone-input.tsx → text-inputs/phone-input.tsx} +0 -0
  147. /package/templates/form/{url-input.tsx → text-inputs/url-input.tsx} +0 -0
  148. /package/templates/{overlays → notes/under-dev/overlay}/portal.tsx +0 -0
@@ -0,0 +1,6 @@
1
+ export * from "./core/types";
2
+ export * from "./core/registry";
3
+ export * from "./core/key-matcher";
4
+ export * from "./hooks/useHotkey";
5
+ export * from "./components/HotkeyProvider";
6
+ export * from "./components/HotkeysHelpModal";
@@ -1,6 +1,6 @@
1
1
  import { useState, useLayoutEffect, useEffect } from "react";
2
2
  import { Menu, PanelLeftOpen, PanelRightOpen, X } from "lucide-react";
3
- import { cn } from "@/utils/cn";
3
+ import { cn } from "@/pejay-ui/utils/cn";
4
4
  import { MenuSection, SidebarMenu } from "./sidebar-menu";
5
5
 
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import { cn } from "@/utils/cn";
2
+ import { cn } from "@/pejay-ui/utils/cn";
3
3
 
4
4
  // ============================================================================
5
5
  // Types
@@ -0,0 +1,606 @@
1
+ # AppProvider, Side Panel & Modals — Project Roadmap
2
+
3
+ > How overlays are wired in FleetNexa frontend: architecture, call chain, and a full Customer Form walkthrough.
4
+
5
+ ---
6
+
7
+ ## Analysis Steps Performed
8
+
9
+ These are the steps taken to map the overlay system from app bootstrap to a real feature (Customer Form):
10
+
11
+ | Step | What was inspected | Why |
12
+ |------|-------------------|-----|
13
+ | 1 | `src/main.tsx` → `src/App.tsx` | Find where `AppProvider` is mounted in the React tree |
14
+ | 2 | `src/provider/AppProvider.tsx` | Understand the overlay stack, `open` / `close`, and rendering logic |
15
+ | 3 | `src/provider/app-context.ts` | See how context is exposed via `useAppProvider()` |
16
+ | 4 | `src/types/provider.interface.ts` | Learn `OverlayContent`, `OverlayOptions`, and stack item types |
17
+ | 5 | `src/hooks/useOverlay.ts` | Find the public API: `openSidePanel`, `openModal` |
18
+ | 6 | `src/hooks/useFormPanel.tsx` | See the form-specific wrapper over side panels |
19
+ | 7 | `src/components/base/overlay/side-panel.tsx` | Side panel animation, portal, and `SidePanelContent` |
20
+ | 8 | `src/components/base/overlay/modal.tsx` + `backdrop.tsx` | Modal rendering and backdrop behavior |
21
+ | 9 | `src/components/base/card/side-panel-card.tsx` | Standard UI shell for form side panels |
22
+ | 10 | `src/components/base/card/modal-card.tsx` | Standard UI shell for modal dialogs |
23
+ | 11 | `src/hooks/form-overlay-registry.ts` + `overlay-close.ts` | Unsaved-changes guard, Escape, keyboard shortcuts |
24
+ | 12 | `src/provider/use-keyboard-shortcuts.tsx` | Global shortcuts when overlays are open |
25
+ | 13 | Grep across routes | Find all consumers: `useFormPanel`, `useOverlay`, domain hooks |
26
+ | 14 | `src/routes/customers/*` | Trace one complete real example end-to-end |
27
+
28
+ ---
29
+
30
+ ## High-Level Architecture
31
+
32
+ ```
33
+ main.tsx
34
+ └── App.tsx
35
+ └── AppProvider ← owns overlay stack (modals + side panels)
36
+ └── RouterProvider ← all pages live here
37
+ └── [stack renders] ← Modal / SidePanel components portaled on top
38
+ ```
39
+
40
+ **Key idea:** You never import `<Modal>` or `<SidePanel>` directly in a page. You call `openModal()` or `openSidePanel()` (usually via hooks), and `AppProvider` renders the overlay on top of the app.
41
+
42
+ ---
43
+
44
+ ## Layer 1 — App Bootstrap & Provider Mount
45
+
46
+ ### Entry point
47
+
48
+ ```tsx
49
+ // src/main.tsx
50
+ createRoot(document.getElementById("root")!).render(<App />);
51
+ ```
52
+
53
+ ### Provider tree
54
+
55
+ ```tsx
56
+ // src/App.tsx
57
+ <StoreProvider>
58
+ <QueryProvider>
59
+ <RolesBootstrap>
60
+ <AppProvider> {/* ← overlay system lives here */}
61
+ <RouterProvider router={router} />
62
+ </AppProvider>
63
+ </RolesBootstrap>
64
+ </QueryProvider>
65
+ <ToastContainer />
66
+ <NotifyContainer />
67
+ </StoreProvider>
68
+ ```
69
+
70
+ `AppProvider` wraps the entire router so **any route** can open modals or side panels.
71
+
72
+ ---
73
+
74
+ ## Layer 2 — AppProvider (The Overlay Engine)
75
+
76
+ **File:** `src/provider/AppProvider.tsx`
77
+
78
+ ### What it stores
79
+
80
+ A **stack** of overlay items. Each item has:
81
+
82
+ | Field | Type | Purpose |
83
+ |-------|------|---------|
84
+ | `id` | `string` | UUID — used to close a specific overlay |
85
+ | `type` | `"modal"` \| `"side-panel"` | Which component to render |
86
+ | `content` | `(helpers) => ReactNode` | Render function; receives `{ close }` |
87
+ | `options` | `{ onSide?: "left" \| "right", ... }` | Passed to Modal / SidePanel |
88
+
89
+ ### Core API (via context)
90
+
91
+ ```ts
92
+ open(type, content, options?) // push onto stack
93
+ close(id) // remove from stack
94
+ isOverlayOpen // stack.length > 0
95
+ stackDepth // how many overlays are open (supports stacking)
96
+ ```
97
+
98
+ ### How it renders
99
+
100
+ ```tsx
101
+ {stack.map((item, index) => {
102
+ const isActive = item.id === topOverlayId; // only top overlay is interactive
103
+ const layer = index + 1; // z-index layering
104
+
105
+ if (item.type === "modal") → <Modal>...</Modal>
106
+ if (item.type === "side-panel") → <SidePanel><SidePanelContent /></SidePanel>
107
+ })}
108
+ ```
109
+
110
+ ### Stacking behavior
111
+
112
+ - Multiple overlays can be open at once (e.g. side panel + modal on top).
113
+ - Only the **top** overlay is `isActive = true` (receives keyboard focus, Escape, backdrop clicks).
114
+ - Lower overlays stay mounted but invisible / non-interactive until the top one closes.
115
+
116
+ ---
117
+
118
+ ## Layer 3 — Public Hooks (How Pages Call Overlays)
119
+
120
+ ### `useAppProvider()` — low-level
121
+
122
+ **File:** `src/provider/app-context.ts`
123
+
124
+ Direct access to `open`, `close`, `stack`, `isOverlayOpen`, `stackDepth`.
125
+
126
+ > Throws if used outside `<AppProvider>`.
127
+
128
+ ### `useOverlay()` — recommended public API
129
+
130
+ **File:** `src/hooks/useOverlay.ts`
131
+
132
+ ```ts
133
+ const { openSidePanel, openModal, isOverlayOpen, stackDepth } = useOverlay();
134
+
135
+ // Side panel — defaults to right side
136
+ openSidePanel(
137
+ ({ close }) => <MyComponent close={close} />,
138
+ { onSide: "left" } // optional
139
+ );
140
+
141
+ // Modal — centered dialog
142
+ openModal(
143
+ ({ close }) => <MyDialog close={close} />
144
+ );
145
+ ```
146
+
147
+ Both call `open()` internally with `APP_PROVIDER_TYPE.MODAL` or `APP_PROVIDER_TYPE.SIDE_PANEL`.
148
+
149
+ ### `useFormPanel()` — form shortcut
150
+
151
+ **File:** `src/hooks/useFormPanel.tsx`
152
+
153
+ Standard pattern for CRUD forms in side panels:
154
+
155
+ ```ts
156
+ const openForm = useFormPanel();
157
+
158
+ openForm(CustomerForm, { mode: "create" });
159
+ // equivalent to:
160
+ // openSidePanel(({ close }) => <CustomerForm mode="create" close={close} />)
161
+ ```
162
+
163
+ Every form component **must** accept a `close: () => void` prop.
164
+
165
+ ---
166
+
167
+ ## Layer 4 — Side Panel Internals
168
+
169
+ **File:** `src/components/base/overlay/side-panel.tsx`
170
+
171
+ ### Flow
172
+
173
+ ```
174
+ AppProvider pushes side-panel item
175
+ → SidePanel mounts in Portal
176
+ → AnimatePresence slides panel from right (or left)
177
+ → Backdrop blur overlay behind it
178
+ → SidePanelContent calls content({ close: requestClose })
179
+ → requestClose triggers exit animation
180
+ → onExitComplete → AppProvider.close(id) removes from stack
181
+ ```
182
+
183
+ ### Important distinction: two `close` functions
184
+
185
+ | Close function | What it does |
186
+ |----------------|--------------|
187
+ | AppProvider's `close(id)` | Immediately removes item from stack |
188
+ | `useAnimatedSidePanelClose()` | Starts exit animation first, then stack removal |
189
+
190
+ `SidePanelContent` wires the **animated** close into your content:
191
+
192
+ ```tsx
193
+ export function SidePanelContent({ content }) {
194
+ const requestClose = useAnimatedSidePanelClose();
195
+ return content?.({ close: requestClose }) ?? null;
196
+ }
197
+ ```
198
+
199
+ So when your form calls `close()`, the panel slides out smoothly.
200
+
201
+ ### Side panel UI shell — `SidePanelCard`
202
+
203
+ **File:** `src/components/base/card/side-panel-card.tsx`
204
+
205
+ Used by almost all form side panels. Provides:
206
+
207
+ - Title, description, header slot (tabs)
208
+ - Scrollable body
209
+ - Footer (Cancel / Save buttons)
210
+ - X button and unsaved-changes protection
211
+ - Keyboard shortcut registration (`Ctrl+Enter` to submit, `Alt+←/→` for tabs)
212
+
213
+ ---
214
+
215
+ ## Layer 5 — Modal Internals
216
+
217
+ **File:** `src/components/base/overlay/modal.tsx`
218
+
219
+ ### Flow
220
+
221
+ ```
222
+ AppProvider pushes modal item
223
+ → Modal mounts in Portal
224
+ → Backdrop (centered, dark overlay)
225
+ → content({ close }) rendered as children
226
+ → close() → AppProvider.close(id) (no slide animation)
227
+ ```
228
+
229
+ ### Modal UI shell — `ModalCard`
230
+
231
+ **File:** `src/components/base/card/modal-card.tsx`
232
+
233
+ Centered white card with title, body, footer, and optional close button.
234
+
235
+ ### When modals are used vs side panels
236
+
237
+ | Use case | Overlay type | Example |
238
+ |----------|-------------|---------|
239
+ | Create / Edit entity forms | Side panel | Customer, Agent, Shipper forms |
240
+ | Read-only detail / quick action | Modal | View Asset dialog |
241
+ | Help / info dialogs | Modal | Keyboard shortcuts (`Shift+?`) |
242
+ | Nested sub-forms inside a page | Side panel | Create Load → other charges panel |
243
+
244
+ ---
245
+
246
+ ## Layer 6 — Close Behavior & Keyboard Shortcuts
247
+
248
+ ### Unsaved changes guard
249
+
250
+ **Files:** `src/hooks/overlay-close.ts`, `src/hooks/form-overlay-registry.ts`
251
+
252
+ When user tries to close (X, Escape, backdrop click):
253
+
254
+ ```
255
+ requestOverlayCloseWithConfirm()
256
+ → Is unsaved confirm already open? → dismiss notify
257
+ → Is form close blocked (async in progress)? → do nothing
258
+ → Is form dirty? → show "Unsaved changes" notify
259
+ → Otherwise → call active close handler
260
+ ```
261
+
262
+ Forms register themselves via `useFormOverlayRegistration()` inside `SidePanelCard`.
263
+
264
+ ### Global keyboard shortcuts (when overlay open)
265
+
266
+ **File:** `src/provider/use-keyboard-shortcuts.tsx`
267
+
268
+ | Shortcut | Action |
269
+ |----------|--------|
270
+ | `Escape` | Close top overlay (with unsaved guard) |
271
+ | `Ctrl/Cmd + Enter` | Submit active form |
272
+ | `Alt + ←` / `Alt + →` | Previous / next form tab |
273
+ | `Shift + ?` | Open keyboard shortcuts help modal |
274
+
275
+ When **no** overlay is open:
276
+
277
+ | Shortcut | Action |
278
+ |----------|--------|
279
+ | `Ctrl/Cmd + Alt + N` | Trigger page "new record" handler |
280
+
281
+ Pages register the new-record handler via `usePageShortcut()`.
282
+
283
+ ---
284
+
285
+ ## Domain Hook Pattern (Used Across Routes)
286
+
287
+ Most entity pages follow this 3-layer pattern:
288
+
289
+ ```
290
+ Page (index.tsx)
291
+ └── useXxxFormPanel() ← domain hook
292
+ └── useFormPanel() ← generic form opener
293
+ └── useOverlay() ← openSidePanel
294
+ └── useAppProvider() → open()
295
+ ```
296
+
297
+ ### All form-panel hooks in the project
298
+
299
+ | Hook | Route | Form component |
300
+ |------|-------|----------------|
301
+ | `useCustomerFormPanel` | `/customers` | `CustomerForm` |
302
+ | `useAgentFormPanel` | `/agents` | `AgentForm` |
303
+ | `useShipperFormPanel` | `/shippers` | `ShipperForm` |
304
+ | `useConsigneeFormPanel` | `/consignees` | `ConsigneeForm` |
305
+ | `useCarrierFormPanel` | `/external-carriers` | `CarrierForm` |
306
+ | `useFactoringFormPanel` | `/factoring-company` | `FactoringForm` |
307
+ | `useRoleFormPanel` | `/roles` | `RoleForm` |
308
+ | `useFleetAddPanel` | `/fleet` | Fleet add form |
309
+ | `useViewAssetPanel` | `/fleet` | `ViewAssetDialog` (**modal**, not side panel) |
310
+
311
+ ---
312
+
313
+ ## Real Example — Customer Form (Start to End)
314
+
315
+ This is the complete call chain when a user clicks **"Add Customer"** on the Customers page.
316
+
317
+ ### Visual flow
318
+
319
+ ```
320
+ User clicks "Add Customer"
321
+
322
+
323
+ CustomersPage ──► useCustomerFormPanel()
324
+
325
+
326
+ openCustomerForm("create")
327
+
328
+
329
+ useFormPanel() ──► openSidePanel(({ close }) => <CustomerForm mode="create" close={close} />)
330
+
331
+
332
+ useOverlay() ──► open(APP_PROVIDER_TYPE.SIDE_PANEL, content, { onSide: "right" })
333
+
334
+
335
+ AppProvider.open() ──► stack.push({ id, type: "side-panel", content, options })
336
+
337
+
338
+ AppProvider re-renders ──► <SidePanel><SidePanelContent content={content} /></SidePanel>
339
+
340
+
341
+ SidePanelContent ──► content({ close: animatedClose })
342
+
343
+
344
+ <CustomerForm mode="create" close={animatedClose} />
345
+
346
+
347
+ <SidePanelCard title="Add Customer" ...>
348
+ <CustomerTab /> or <AdvancedTab />
349
+ </SidePanelCard>
350
+
351
+
352
+ User clicks Save ──► handleSave() ──► toast.success() ──► close()
353
+
354
+
355
+ Animated slide-out ──► AppProvider.close(id) ──► panel removed from DOM
356
+ ```
357
+
358
+ ---
359
+
360
+ ### Step-by-step with real file references
361
+
362
+ #### Step 1 — Page triggers the panel
363
+
364
+ **File:** `src/routes/customers/index.tsx`
365
+
366
+ ```tsx
367
+ const openCustomerForm = useCustomerFormPanel();
368
+ usePageShortcut(() => openCustomerForm("create")); // Ctrl+Alt+N
369
+
370
+ // "Add Customer" button
371
+ <Btn onClick={() => openCustomerForm("create")}>Add Customer</Btn>
372
+
373
+ // Table row actions (view / edit)
374
+ const handleEdit = (customer) => openCustomerForm("update", customer);
375
+ ```
376
+
377
+ Three entry points, same hook:
378
+ - Header button → create mode
379
+ - Keyboard shortcut → create mode
380
+ - Table row → update mode with existing `customer` data
381
+
382
+ ---
383
+
384
+ #### Step 2 — Domain hook wraps the form component
385
+
386
+ **File:** `src/routes/customers/hooks/use-customer-form-panel.tsx`
387
+
388
+ ```tsx
389
+ export function useCustomerFormPanel() {
390
+ const openForm = useFormPanel();
391
+
392
+ return useCallback(
393
+ (mode: CustomerFormMode = "create", customer?: Customer) => {
394
+ openForm(CustomerForm, { mode, customer });
395
+ },
396
+ [openForm],
397
+ );
398
+ }
399
+ ```
400
+
401
+ This hook knows **which component** and **which props** — the page doesn't need to know about `useOverlay` or `openSidePanel`.
402
+
403
+ ---
404
+
405
+ #### Step 3 — Generic form panel opens side panel
406
+
407
+ **File:** `src/hooks/useFormPanel.tsx`
408
+
409
+ ```tsx
410
+ export function useFormPanel() {
411
+ const { openSidePanel } = useOverlay();
412
+
413
+ return useCallback(
414
+ (Component, props) => {
415
+ openSidePanel(({ close }) => (
416
+ <Component {...props} close={close} />
417
+ ));
418
+ },
419
+ [openSidePanel],
420
+ );
421
+ }
422
+ ```
423
+
424
+ Injects `close` into the form component automatically.
425
+
426
+ ---
427
+
428
+ #### Step 4 — useOverlay pushes to AppProvider stack
429
+
430
+ **File:** `src/hooks/useOverlay.ts`
431
+
432
+ ```tsx
433
+ openSidePanel(content, options) {
434
+ open(APP_PROVIDER_TYPE.SIDE_PANEL, content, { onSide: "right", ...options });
435
+ }
436
+ ```
437
+
438
+ ---
439
+
440
+ #### Step 5 — AppProvider renders SidePanel
441
+
442
+ **File:** `src/provider/AppProvider.tsx`
443
+
444
+ ```tsx
445
+ if (item.type === APP_PROVIDER_TYPE.SIDE_PANEL) {
446
+ return (
447
+ <SidePanel key={item.id} options={...} isActive={...} onClose={() => close(item.id)}>
448
+ <SidePanelContent content={item.content} />
449
+ </SidePanel>
450
+ );
451
+ }
452
+ ```
453
+
454
+ ---
455
+
456
+ #### Step 6 — CustomerForm renders inside SidePanelCard
457
+
458
+ **File:** `src/routes/customers/components/customer-form/index.tsx`
459
+
460
+ ```tsx
461
+ export function CustomerForm({ close, mode = "create", customer }) {
462
+ const [values, setValues] = useState(() => ({
463
+ ...getDefaultCustomerFormValues(),
464
+ ...(customer ? customerToFormValues(customer) : {}),
465
+ }));
466
+
467
+ const handleSave = () => {
468
+ if (!values.customerName.trim()) {
469
+ toast.error("Customer name is required");
470
+ return;
471
+ }
472
+ close(); // ← closes side panel
473
+ toast.success("Customer added successfully!");
474
+ };
475
+
476
+ return (
477
+ <SidePanelCard
478
+ title={mode === "update" ? "Edit Customer" : "Add Customer"}
479
+ close={close}
480
+ onSubmit={handleSave}
481
+ isDirty={isDirty}
482
+ formTabs={...}
483
+ footer={
484
+ <>
485
+ <Btn onClick={requestOverlayCloseWithConfirm}>Cancel</Btn>
486
+ <Btn onClick={handleSave}>Save</Btn>
487
+ </>
488
+ }
489
+ >
490
+ {activeTab === "customer"
491
+ ? <CustomerTab values={values} setField={setField} />
492
+ : <AdvancedTab values={values} setField={setField} />}
493
+ </SidePanelCard>
494
+ );
495
+ }
496
+ ```
497
+
498
+ **SidePanelCard** registers the form with the overlay registry so Escape / backdrop / dirty-check all work.
499
+
500
+ ---
501
+
502
+ #### Step 7 — User closes the panel
503
+
504
+ | User action | What happens |
505
+ |-------------|--------------|
506
+ | Click **Save** | `handleSave()` → validation → `close()` → slide out → removed from stack |
507
+ | Click **Cancel** | `requestOverlayCloseWithConfirm()` → if dirty, shows notify → else `close()` |
508
+ | Click **X** | Same as Cancel (via `SidePanelCard.handleClose`) |
509
+ | Click **backdrop** | `requestOverlayCloseWithConfirm()` in `SidePanel` |
510
+ | Press **Escape** | `useKeyboardShortcuts` → `requestOverlayCloseWithConfirm()` |
511
+ | Press **Ctrl+Enter** | `getActiveFormOverlay().onSubmit()` → `handleSave()` |
512
+
513
+ ---
514
+
515
+ ## Modal Example (For Comparison) — View Asset
516
+
517
+ **File:** `src/routes/fleet/hooks/use-view-asset-panel.tsx`
518
+
519
+ ```tsx
520
+ export function useViewAssetPanel() {
521
+ const { openModal } = useOverlay();
522
+
523
+ return useCallback(
524
+ (asset: FleetTruck) => {
525
+ openModal(({ close }) => <ViewAssetDialog asset={asset} close={close} />);
526
+ },
527
+ [openModal],
528
+ );
529
+ }
530
+ ```
531
+
532
+ **File:** `src/routes/fleet/components/view-asset-dialog.tsx`
533
+
534
+ ```tsx
535
+ export function ViewAssetDialog({ asset, close }) {
536
+ return (
537
+ <ModalCard close={close} title={asset.name} footer={...}>
538
+ {/* read-only fields */}
539
+ </ModalCard>
540
+ );
541
+ }
542
+ ```
543
+
544
+ Same pattern, different shell (`ModalCard` instead of `SidePanelCard`) and different opener (`openModal` instead of `openSidePanel`).
545
+
546
+ ---
547
+
548
+ ## Quick Reference — How to Add a New Form Side Panel
549
+
550
+ 1. **Create form component** with `close: () => void` prop
551
+ 2. **Wrap in `SidePanelCard`** with title, footer, `isDirty`, `onSubmit`
552
+ 3. **Create domain hook** — `useMyEntityFormPanel()` using `useFormPanel()`
553
+ 4. **Call from page** — button click, table action, or `usePageShortcut()`
554
+
555
+ ```tsx
556
+ // hooks/use-my-entity-form-panel.tsx
557
+ export function useMyEntityFormPanel() {
558
+ const openForm = useFormPanel();
559
+ return useCallback(
560
+ (mode = "create", entity?) => openForm(MyEntityForm, { mode, entity }),
561
+ [openForm],
562
+ );
563
+ }
564
+
565
+ // pages/my-entity/index.tsx
566
+ const openForm = useMyEntityFormPanel();
567
+ <Btn onClick={() => openForm("create")}>Add Entity</Btn>
568
+ ```
569
+
570
+ No changes needed in `AppProvider` — it already handles everything.
571
+
572
+ ---
573
+
574
+ ## File Map
575
+
576
+ ```
577
+ src/
578
+ ├── App.tsx # AppProvider mount point
579
+ ├── provider/
580
+ │ ├── AppProvider.tsx # Overlay stack engine
581
+ │ ├── app-context.ts # useAppProvider()
582
+ │ └── use-keyboard-shortcuts.tsx # Global overlay shortcuts
583
+ ├── hooks/
584
+ │ ├── useOverlay.ts # openSidePanel / openModal
585
+ │ ├── useFormPanel.tsx # Generic form side panel opener
586
+ │ ├── overlay-close.ts # Unsaved changes close logic
587
+ │ ├── form-overlay-registry.ts # Active form registration
588
+ │ └── useFormOverlayRegistration.ts # Hook used by SidePanelCard
589
+ ├── components/base/
590
+ │ ├── overlay/
591
+ │ │ ├── side-panel.tsx # Animated side panel
592
+ │ │ ├── modal.tsx # Centered modal
593
+ │ │ └── backdrop.tsx # Modal backdrop
594
+ │ └── card/
595
+ │ ├── side-panel-card.tsx # Form panel UI shell
596
+ │ └── modal-card.tsx # Modal dialog UI shell
597
+ ├── types/
598
+ │ └── provider.interface.ts # Overlay types
599
+ └── routes/customers/
600
+ ├── index.tsx # Page — triggers panel
601
+ ├── hooks/use-customer-form-panel.tsx
602
+ └── components/customer-form/index.tsx
603
+ ```
604
+
605
+ ---
606
+