pejay-ui 1.4.3 → 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.
- package/README.md +26 -0
- package/bin/cli.js +45 -15
- package/package.json +2 -1
- package/registry/buttons.json +3 -2
- package/registry/dropdowns.json +3 -1
- package/registry/forms.json +51 -23
- package/registry/hotkeys.json +12 -0
- package/registry/overlays.json +18 -2
- package/registry/panels.json +21 -0
- package/registry/skeleton.json +20 -0
- package/registry/spinner.json +13 -0
- package/templates/button/Button.tsx +8 -7
- package/templates/button/README.md +81 -0
- package/templates/button/index.ts +1 -2
- package/templates/form/{checkbox-group.tsx → choices/checkbox-group.tsx} +1 -1
- package/templates/form/{checkbox.tsx → choices/checkbox.tsx} +1 -1
- package/templates/form/{radio-group.tsx → choices/radio-group.tsx} +1 -1
- package/templates/form/{radio.tsx → choices/radio.tsx} +1 -1
- package/templates/form/choices/readme.checkbox-group.md +27 -0
- package/templates/form/choices/readme.checkbox.md +26 -0
- package/templates/form/choices/readme.radio-group.md +26 -0
- package/templates/form/choices/readme.radio.md +24 -0
- package/templates/form/choices/readme.switch.md +26 -0
- package/templates/form/{switch.tsx → choices/switch.tsx} +1 -1
- package/templates/form/{file-input.tsx → file/file-input.tsx} +2 -2
- package/templates/form/file/readme.file-input.md +26 -0
- package/templates/form/index.ts +19 -22
- package/templates/form/{amount-input.tsx → numeric/amount-input.tsx} +2 -2
- package/templates/form/{number-input.tsx → numeric/number-input.tsx} +2 -2
- package/templates/form/{range-slider.tsx → numeric/range-slider.tsx} +1 -1
- package/templates/form/numeric/readme.amount-input.md +27 -0
- package/templates/form/numeric/readme.number-input.md +26 -0
- package/templates/form/numeric/readme.range-slider.md +27 -0
- package/templates/form/{date-picker.tsx → pickers/date-picker.tsx} +2 -2
- package/templates/form/{date-range-picker.tsx → pickers/date-range-picker.tsx} +2 -2
- package/templates/form/pickers/readme.date-picker.md +26 -0
- package/templates/form/pickers/readme.date-range-picker.md +25 -0
- package/templates/form/pickers/readme.time-picker.md +25 -0
- package/templates/form/pickers/readme.time-range-picker.md +25 -0
- package/templates/form/{time-picker.tsx → pickers/time-picker.tsx} +1 -1
- package/templates/form/{time-range-picker.tsx → pickers/time-range-picker.tsx} +1 -1
- package/templates/form/{input.tsx → text-inputs/input.tsx} +1 -1
- package/templates/form/{password-input.tsx → text-inputs/password-input.tsx} +1 -1
- package/templates/form/text-inputs/readme.email-input.md +24 -0
- package/templates/form/text-inputs/readme.input.md +28 -0
- package/templates/form/text-inputs/readme.password-input.md +24 -0
- package/templates/form/text-inputs/readme.phone-input.md +24 -0
- package/templates/form/text-inputs/readme.textarea.md +24 -0
- package/templates/form/text-inputs/readme.url-input.md +23 -0
- package/templates/form/{textarea.tsx → text-inputs/textarea.tsx} +1 -1
- package/templates/hotkeys/README.md +134 -0
- package/templates/hotkeys/components/HotkeyProvider.tsx +78 -0
- package/templates/hotkeys/components/HotkeysHelpModal.tsx +102 -0
- package/templates/hotkeys/core/key-matcher.ts +106 -0
- package/templates/hotkeys/core/registry.ts +39 -0
- package/templates/hotkeys/core/types.ts +15 -0
- package/templates/hotkeys/hooks/useHotkey.ts +43 -0
- package/templates/hotkeys/index.ts +6 -0
- package/templates/layouts/lv1/app-layout.tsx +1 -1
- package/templates/layouts/lv1/sidebar-menu.tsx +1 -1
- package/templates/notes/app-provider/app-provider-side-panel-modals-roadmap.md +606 -0
- package/templates/notes/app-provider/manual-open-close-side-panel-and-modal.md +913 -0
- package/templates/notes/app-provider/side-panel-card-hooks-and-complexity.md +578 -0
- package/templates/notes/under-dev/AppProvider.tsx +92 -0
- package/templates/notes/under-dev/app-context.ts +14 -0
- package/templates/notes/under-dev/card/base-card.tsx +35 -0
- package/templates/notes/under-dev/card/index.ts +4 -0
- package/templates/notes/under-dev/card/modal-card.tsx +88 -0
- package/templates/notes/under-dev/card/side-panel-card.tsx +127 -0
- package/templates/notes/under-dev/form-overlay-registry.ts +42 -0
- package/templates/notes/under-dev/keyboard-shortcuts-help.tsx +79 -0
- package/templates/notes/under-dev/keyboard-utils.ts +22 -0
- package/templates/notes/under-dev/overlay/backdrop.tsx +95 -0
- package/templates/notes/under-dev/overlay/index.ts +4 -0
- package/templates/notes/under-dev/overlay/modal.tsx +43 -0
- package/templates/notes/under-dev/overlay/side-panel.tsx +126 -0
- package/templates/notes/under-dev/overlay-close.ts +50 -0
- package/templates/notes/under-dev/page-shortcut-registry.ts +9 -0
- package/templates/notes/under-dev/unsaved-changes-notify.ts +11 -0
- package/templates/notes/under-dev/use-keyboard-shortcuts.tsx +110 -0
- package/templates/notes/under-dev/useFormDirty.ts +6 -0
- package/templates/notes/under-dev/useFormOverlayRegistration.ts +47 -0
- package/templates/notes/under-dev/useFormPanel.tsx +18 -0
- package/templates/notes/under-dev/useFormTabHandler.ts +22 -0
- package/templates/notes/under-dev/useHorizontalWheelScroll.ts +27 -0
- package/templates/notes/under-dev/useOverlay.ts +41 -0
- package/templates/overlays/index.ts +2 -1
- package/templates/overlays/portal/portal.tsx +26 -0
- package/templates/overlays/tooltip/readme.tooltip.md +26 -0
- package/templates/{button → overlays/tooltip}/tooltip.tsx +1 -1
- package/templates/panels/COMPONENTS.md +103 -0
- package/templates/panels/README.md +702 -0
- package/templates/panels/components/base-card.tsx +33 -0
- package/templates/panels/components/index.ts +8 -0
- package/templates/panels/components/modal/backdrop.tsx +88 -0
- package/templates/panels/components/modal/modal-card.tsx +139 -0
- package/templates/panels/components/modal/modal-raw.tsx +36 -0
- package/templates/panels/components/modal/modal.tsx +49 -0
- package/templates/panels/components/side-panel/side-panel-card.tsx +123 -0
- package/templates/panels/components/side-panel/side-panel-raw.tsx +25 -0
- package/templates/panels/components/side-panel/side-panel.tsx +135 -0
- package/templates/panels/core/PanelProvider.tsx +145 -0
- package/templates/panels/core/constants.ts +9 -0
- package/templates/panels/core/form-overlay-registry.ts +35 -0
- package/templates/panels/core/index.ts +6 -0
- package/templates/panels/core/overlay-close.ts +11 -0
- package/templates/panels/core/panel-context.ts +41 -0
- package/templates/panels/core/types.ts +41 -0
- package/templates/panels/hooks/index.ts +7 -0
- package/templates/panels/hooks/useFormDirty.ts +6 -0
- package/templates/panels/hooks/useFormOverlayRegistration.ts +92 -0
- package/templates/panels/hooks/useFormPanel.tsx +18 -0
- package/templates/panels/hooks/useFormTabHandler.ts +25 -0
- package/templates/panels/hooks/useHorizontalWheelScroll.ts +31 -0
- package/templates/panels/hooks/useModalForm.tsx +22 -0
- package/templates/panels/hooks/useOverlay.ts +65 -0
- package/templates/panels/index.ts +3 -0
- package/templates/panels/vendor-example/by-using-modal/VendorModalPage.tsx +47 -0
- package/templates/panels/vendor-example/by-using-modal/useVendorModalForm.tsx +19 -0
- package/templates/panels/vendor-example/by-using-modal/vendor-modal-form.tsx +112 -0
- package/templates/panels/vendor-example/by-using-modal/vendor-types.ts +29 -0
- package/templates/panels/vendor-example/by-using-sidepanel/VendorsPage.tsx +47 -0
- package/templates/panels/vendor-example/by-using-sidepanel/useVendorFormPanel.tsx +15 -0
- package/templates/panels/vendor-example/by-using-sidepanel/vendor-form.tsx +108 -0
- package/templates/panels/vendor-example/by-using-sidepanel/vendor-types.ts +29 -0
- package/templates/select-dropdown/README.md +62 -0
- package/templates/select-dropdown/multiselect-input.tsx +2 -2
- package/templates/select-dropdown/select-input.tsx +2 -2
- package/templates/skeleton/README.md +53 -0
- package/templates/skeleton/index.ts +2 -0
- package/templates/skeleton/skeleton.css +36 -0
- package/templates/skeleton/skeleton.tsx +40 -0
- package/templates/skeleton/types.ts +12 -0
- package/templates/spinner/README.md +51 -0
- package/templates/spinner/index.ts +1 -0
- package/templates/spinner/spinner.css +58 -0
- package/templates/spinner/spinner.tsx +263 -0
- package/templates/toast/container.tsx +2 -2
- package/templates/utilities/formater.dateTime.md +74 -0
- package/templates/utilities/formater.dateTime.ts +310 -0
- package/templates/utilities/formater.phoneNumber.md +32 -0
- package/templates/utilities/formater.phoneNumber.ts +143 -0
- package/templates/utilities/sanitize.md +23 -0
- package/templates/utilities/sanitize.ts +148 -0
- /package/templates/form/{email-input.tsx → text-inputs/email-input.tsx} +0 -0
- /package/templates/form/{phone-input.tsx → text-inputs/phone-input.tsx} +0 -0
- /package/templates/form/{url-input.tsx → text-inputs/url-input.tsx} +0 -0
- /package/templates/{overlays → notes/under-dev/overlay}/portal.tsx +0 -0
|
@@ -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
|
+
|