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,702 @@
1
+ # Panels Overlay Manager
2
+
3
+ A modular, stack-safe, and keyboard-friendly Side Panel and Modal manager for React. Supports overlay stacking, split context to prevent re-renders, and automatic keyboard shortcuts.
4
+
5
+ ---
6
+
7
+ ## The One Golden Rule
8
+
9
+ > **Never render `<SidePanel>` or `<Modal>` directly inside a page.**
10
+ >
11
+ > Always trigger overlays through a hook. `PanelProvider` renders everything via a React Portal on top of your entire app — you never place overlay components inside your page tree.
12
+
13
+ ---
14
+
15
+ ## Quick Start — Setup (Do This Once)
16
+
17
+ Wrap your root application in `PanelProvider` (in `src/main.tsx` or `src/App.tsx`):
18
+
19
+ ```tsx
20
+ import { PanelProvider } from "@/pejay-ui/panels";
21
+
22
+ export default function App() {
23
+ return (
24
+ <PanelProvider>
25
+ <YourApp />
26
+ </PanelProvider>
27
+ );
28
+ }
29
+ ```
30
+
31
+ > **Important:** If `PanelProvider` is missing, **no overlay will ever appear**. Always do this step first.
32
+
33
+ ---
34
+
35
+ ## The 4 Overlay Types
36
+
37
+ There are 4 ways to open an overlay. Choose the one that fits your use case:
38
+
39
+ | Type | Hook | Card | Best for |
40
+ | :------------------ | :------------------------------ | :-------------- | :----------------------------------------------- |
41
+ | **Form Side Panel** | `useFormPanel()` | `SidePanelCard` | Create / Edit forms in a slide-in panel |
42
+ | **Form Modal** | `useModalForm()` | `ModalCard` | Create / Edit forms in a centered dialog |
43
+ | **Raw Side Panel** | `useOverlay().openRawSidePanel` | `SidePanelRaw` | Fully custom side panel — you control everything |
44
+ | **Raw Modal** | `useOverlay().openRawModal` | `ModalRaw` | Fully custom modal — you control everything |
45
+
46
+ See the `vendor-example/` folder for a working reference of all 4 types.
47
+
48
+ ---
49
+
50
+ ## Pattern 1 — Form Side Panel (4-File Pattern)
51
+
52
+ Use this for create/edit forms. See `vendor-example/by-using-sidepanel/` for the full working example.
53
+
54
+ ### File 1 — Types (`your-types.ts`)
55
+
56
+ ```tsx
57
+ // The entity model (from your API / table row)
58
+ export type Vendor = {
59
+ id: string;
60
+ name: string;
61
+ email: string;
62
+ phone: string;
63
+ };
64
+
65
+ // The form field values (can differ from entity shape)
66
+ export type VendorFormValues = {
67
+ vendorName: string;
68
+ vendorEmail: string;
69
+ vendorPhone: string;
70
+ };
71
+
72
+ export function getDefaultVendorFormValues(): VendorFormValues {
73
+ return { vendorName: "", vendorEmail: "", vendorPhone: "" };
74
+ }
75
+
76
+ export function vendorToFormValues(vendor: Vendor): VendorFormValues {
77
+ return {
78
+ vendorName: vendor.name,
79
+ vendorEmail: vendor.email,
80
+ vendorPhone: vendor.phone,
81
+ };
82
+ }
83
+ ```
84
+
85
+ ### File 2 — Form Component (`your-form.tsx`)
86
+
87
+ Rules every form overlay component **must** follow:
88
+
89
+ 1. Accept `close: () => void` — injected automatically by the hook.
90
+ 2. Wrap content in `<SidePanelCard>`.
91
+ 3. Call `close()` only after a successful save.
92
+ 4. Use `requestOverlayCloseWithConfirm` on Cancel so the dirty guard works.
93
+
94
+ ```tsx
95
+ import { useState } from "react";
96
+ import {
97
+ SidePanelCard,
98
+ useFormDirty,
99
+ requestOverlayCloseWithConfirm,
100
+ } from "@/pejay-ui/panels";
101
+ import type { Vendor } from "./your-types";
102
+ import { getDefaultVendorFormValues, vendorToFormValues } from "./your-types";
103
+
104
+ export type VendorFormMode = "create" | "update";
105
+ export type VendorFormProps = {
106
+ close: () => void; // REQUIRED — always accept this
107
+ mode?: VendorFormMode;
108
+ vendor?: Vendor;
109
+ };
110
+
111
+ export function VendorForm({
112
+ close,
113
+ mode = "create",
114
+ vendor,
115
+ }: VendorFormProps) {
116
+ const [values, setValues] = useState(() => ({
117
+ ...getDefaultVendorFormValues(),
118
+ ...(vendor ? vendorToFormValues(vendor) : {}),
119
+ }));
120
+
121
+ const isDirty = useFormDirty(values);
122
+
123
+ const handleSave = () => {
124
+ if (!values.vendorName.trim()) return alert("Name required");
125
+ console.log("Saving:", values);
126
+ close();
127
+ };
128
+
129
+ return (
130
+ <SidePanelCard
131
+ title={mode === "update" ? "Edit Vendor" : "Add Vendor"}
132
+ close={close}
133
+ onSubmit={handleSave} // enables Ctrl+Enter
134
+ isDirty={isDirty} // enables unsaved-changes guard
135
+ size="md"
136
+ footer={
137
+ <div className="flex justify-end gap-3">
138
+ <button onClick={requestOverlayCloseWithConfirm}>Cancel</button>
139
+ <button onClick={handleSave}>Save</button>
140
+ </div>
141
+ }
142
+ >
143
+ <input
144
+ value={values.vendorName}
145
+ onChange={e => setValues(p => ({ ...p, vendorName: e.target.value }))}
146
+ placeholder="Vendor Name"
147
+ />
148
+ </SidePanelCard>
149
+ );
150
+ }
151
+ ```
152
+
153
+ ### File 3 — Domain Hook (`useYourFormPanel.tsx`)
154
+
155
+ ```tsx
156
+ import { useCallback } from "react";
157
+ import { useFormPanel } from "@/pejay-ui/panels";
158
+ import { VendorForm, type VendorFormMode } from "./your-form";
159
+ import type { Vendor } from "./your-types";
160
+
161
+ export function useVendorFormPanel() {
162
+ const openForm = useFormPanel(); // <-- opens as SIDE PANEL
163
+
164
+ return useCallback(
165
+ (mode: VendorFormMode = "create", vendor?: Vendor) => {
166
+ openForm(VendorForm, { mode, vendor });
167
+ },
168
+ [openForm],
169
+ );
170
+ }
171
+ ```
172
+
173
+ ### File 4 — Page Component (`YourPage.tsx`)
174
+
175
+ ```tsx
176
+ import { useVendorFormPanel } from "./useVendorFormPanel";
177
+
178
+ export default function VendorsPage() {
179
+ const openVendorForm = useVendorFormPanel();
180
+
181
+ return (
182
+ <div>
183
+ <button onClick={() => openVendorForm("create")}>Add Vendor</button>
184
+ <button onClick={() => openVendorForm("update", selectedVendor)}>
185
+ Edit Vendor
186
+ </button>
187
+ </div>
188
+ );
189
+ }
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Pattern 2 — Form Modal (4-File Pattern)
195
+
196
+ Same form, same data — but opens in a **centered modal dialog** instead of a side panel. Only Files 2, 3, and 4 change. See `vendor-example/by-using-modal/` for the full working example.
197
+
198
+ ### File 1 — Types (`your-types.ts`)
199
+
200
+ Identical to Pattern 1. The same type definitions work for both side panels and modals.
201
+
202
+ ### File 2 — Form Component (`your-modal-form.tsx`)
203
+
204
+ The only difference from Pattern 1 is the wrapper: use `<ModalCard>` instead of `<SidePanelCard>`. All props are identical.
205
+
206
+ ```tsx
207
+ import { useState } from "react";
208
+ import {
209
+ ModalCard,
210
+ useFormDirty,
211
+ requestOverlayCloseWithConfirm,
212
+ } from "@/pejay-ui/panels";
213
+ import type { Vendor } from "./your-types";
214
+ import { getDefaultVendorFormValues, vendorToFormValues } from "./your-types";
215
+
216
+ export type VendorFormMode = "create" | "update";
217
+ export type VendorModalFormProps = {
218
+ close: () => void;
219
+ mode?: VendorFormMode;
220
+ vendor?: Vendor;
221
+ };
222
+
223
+ export function VendorModalForm({
224
+ close,
225
+ mode = "create",
226
+ vendor,
227
+ }: VendorModalFormProps) {
228
+ const [values, setValues] = useState(() => ({
229
+ ...getDefaultVendorFormValues(),
230
+ ...(vendor ? vendorToFormValues(vendor) : {}),
231
+ }));
232
+
233
+ const isDirty = useFormDirty(values);
234
+
235
+ const handleSave = () => {
236
+ if (!values.vendorName.trim()) return alert("Name required");
237
+ console.log("Saving:", values);
238
+ close();
239
+ };
240
+
241
+ return (
242
+ <ModalCard
243
+ title={mode === "update" ? "Edit Vendor" : "Add Vendor"}
244
+ description="Fill in the vendor details below."
245
+ close={close}
246
+ onSubmit={handleSave} // enables Ctrl+Enter
247
+ isDirty={isDirty} // enables unsaved-changes guard
248
+ size="md"
249
+ footer={
250
+ <div className="flex justify-end gap-3">
251
+ <button onClick={requestOverlayCloseWithConfirm}>Cancel</button>
252
+ <button onClick={handleSave}>Save</button>
253
+ </div>
254
+ }
255
+ >
256
+ <input
257
+ value={values.vendorName}
258
+ onChange={e => setValues(p => ({ ...p, vendorName: e.target.value }))}
259
+ placeholder="Vendor Name"
260
+ />
261
+ </ModalCard>
262
+ );
263
+ }
264
+ ```
265
+
266
+ ### File 3 — Domain Hook (`useYourModalForm.tsx`)
267
+
268
+ Use `useModalForm()` instead of `useFormPanel()` — that's the only change.
269
+
270
+ ```tsx
271
+ import { useCallback } from "react";
272
+ import { useModalForm } from "@/pejay-ui/panels";
273
+ import { VendorModalForm, type VendorFormMode } from "./your-modal-form";
274
+ import type { Vendor } from "./your-types";
275
+
276
+ export function useVendorModalForm() {
277
+ const openForm = useModalForm(); // <-- opens as MODAL
278
+
279
+ return useCallback(
280
+ (mode: VendorFormMode = "create", vendor?: Vendor) => {
281
+ openForm(VendorModalForm, { mode, vendor });
282
+ },
283
+ [openForm],
284
+ );
285
+ }
286
+ ```
287
+
288
+ ### File 4 — Page Component (`YourModalPage.tsx`)
289
+
290
+ ```tsx
291
+ import { useVendorModalForm } from "./useVendorModalForm";
292
+
293
+ export default function VendorModalPage() {
294
+ const openVendorModal = useVendorModalForm();
295
+
296
+ return (
297
+ <div>
298
+ <button onClick={() => openVendorModal("create")}>
299
+ Add Vendor (Modal)
300
+ </button>
301
+ <button onClick={() => openVendorModal("update", selectedVendor)}>
302
+ Edit Vendor (Modal)
303
+ </button>
304
+ </div>
305
+ );
306
+ }
307
+ ```
308
+
309
+ ---
310
+
311
+ ## Pattern 3 — Raw Side Panel (Custom Layout)
312
+
313
+ Use this when you need **100% custom content** in a side panel — no title, no X button, no footer. Your content receives a `{ close }` callback and is fully responsible for its own layout.
314
+
315
+ ### Option A — Quick Inline (single page)
316
+
317
+ Call it directly inside the page. Good for a one-off panel that's only used in one place:
318
+
319
+ ```tsx
320
+ import { useOverlay } from "@/pejay-ui/panels";
321
+
322
+ function MyPage() {
323
+ const { openRawSidePanel } = useOverlay();
324
+
325
+ const handleOpen = () => {
326
+ openRawSidePanel(({ close }) => (
327
+ <div className="p-6 flex flex-col gap-4">
328
+ <h2 className="text-xl font-bold">Custom Side Panel</h2>
329
+ <p>You control the entire layout here.</p>
330
+ <button onClick={close}>Close Me</button>
331
+ </div>
332
+ ));
333
+ };
334
+
335
+ return <button onClick={handleOpen}>Open Custom Side Panel</button>;
336
+ }
337
+ ```
338
+
339
+ ### Option B — Domain Hook (reuse across pages)
340
+
341
+ Follow the same domain hook pattern as Patterns 1 & 2 if you need to open the same custom panel from multiple pages. Create 2 files:
342
+
343
+ **`my-custom-panel.tsx`** — your custom content component:
344
+
345
+ ```tsx
346
+ // Accepts close injected by the hook
347
+ export type MyCustomPanelProps = {
348
+ close: () => void;
349
+ someData?: string; // any extra props your panel needs
350
+ };
351
+
352
+ export function MyCustomPanel({ close, someData }: MyCustomPanelProps) {
353
+ return (
354
+ <div className="p-6 flex flex-col gap-4">
355
+ <h2 className="text-xl font-bold">Custom Side Panel</h2>
356
+ <p>Data: {someData}</p>
357
+ <button onClick={close}>Close</button>
358
+ </div>
359
+ );
360
+ }
361
+ ```
362
+
363
+ **`useMyCustomPanel.tsx`** — the domain hook:
364
+
365
+ ```tsx
366
+ import { useCallback } from "react";
367
+ import { useOverlay } from "@/pejay-ui/panels";
368
+ import { MyCustomPanel } from "./my-custom-panel";
369
+
370
+ export function useMyCustomPanel() {
371
+ const { openRawSidePanel } = useOverlay();
372
+
373
+ return useCallback(
374
+ (someData?: string) => {
375
+ openRawSidePanel(({ close }) => (
376
+ <MyCustomPanel close={close} someData={someData} />
377
+ ));
378
+ },
379
+ [openRawSidePanel],
380
+ );
381
+ }
382
+ ```
383
+
384
+ **Any page** — just call the hook:
385
+
386
+ ```tsx
387
+ import { useMyCustomPanel } from "./useMyCustomPanel";
388
+
389
+ function PageA() {
390
+ const openPanel = useMyCustomPanel();
391
+ return <button onClick={() => openPanel("hello")}>Open Panel</button>;
392
+ }
393
+
394
+ function PageB() {
395
+ const openPanel = useMyCustomPanel();
396
+ return <button onClick={() => openPanel("world")}>Open Panel</button>;
397
+ }
398
+ ```
399
+
400
+ ---
401
+
402
+ ## Pattern 4 — Raw Modal (Custom Layout)
403
+
404
+ Same as Pattern 3, but opens a **centered modal** with no chrome. Follows the exact same two options.
405
+
406
+ ### Option A — Quick Inline (single page)
407
+
408
+ ```tsx
409
+ import { useOverlay } from "@/pejay-ui/panels";
410
+
411
+ function MyPage() {
412
+ const { openRawModal } = useOverlay();
413
+
414
+ const handleOpen = () => {
415
+ openRawModal(({ close }) => (
416
+ <div className="p-6 flex flex-col gap-4">
417
+ <h2 className="text-xl font-bold">Custom Modal</h2>
418
+ <p>You control the entire layout here.</p>
419
+ <button onClick={close}>Dismiss</button>
420
+ </div>
421
+ ));
422
+ };
423
+
424
+ return <button onClick={handleOpen}>Open Custom Modal</button>;
425
+ }
426
+ ```
427
+
428
+ ### Option B — Domain Hook (reuse across pages)
429
+
430
+ **`my-custom-modal.tsx`** — your custom content component:
431
+
432
+ ```tsx
433
+ export type MyCustomModalProps = {
434
+ close: () => void;
435
+ someData?: string;
436
+ };
437
+
438
+ export function MyCustomModal({ close, someData }: MyCustomModalProps) {
439
+ return (
440
+ <div className="p-6 flex flex-col gap-4">
441
+ <h2 className="text-xl font-bold">Custom Modal</h2>
442
+ <p>Data: {someData}</p>
443
+ <button onClick={close}>Dismiss</button>
444
+ </div>
445
+ );
446
+ }
447
+ ```
448
+
449
+ **`useMyCustomModal.tsx`** — the domain hook:
450
+
451
+ ```tsx
452
+ import { useCallback } from "react";
453
+ import { useOverlay } from "@/pejay-ui/panels";
454
+ import { MyCustomModal } from "./my-custom-modal";
455
+
456
+ export function useMyCustomModal() {
457
+ const { openRawModal } = useOverlay();
458
+
459
+ return useCallback(
460
+ (someData?: string) => {
461
+ openRawModal(({ close }) => (
462
+ <MyCustomModal close={close} someData={someData} />
463
+ ));
464
+ },
465
+ [openRawModal],
466
+ );
467
+ }
468
+ ```
469
+
470
+ **Any page** — just call the hook:
471
+
472
+ ```tsx
473
+ import { useMyCustomModal } from "./useMyCustomModal";
474
+
475
+ function PageA() {
476
+ const openModal = useMyCustomModal();
477
+ return <button onClick={() => openModal("hello")}>Open Modal</button>;
478
+ }
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Working Examples
484
+
485
+ ```text
486
+ panels/
487
+ └── vendor-example/
488
+ ├── by-using-sidepanel/ ← Pattern 1 (Form Side Panel)
489
+ │ ├── vendor-types.ts
490
+ │ ├── vendor-form.tsx ← Uses SidePanelCard
491
+ │ ├── useVendorFormPanel.tsx ← Uses useFormPanel()
492
+ │ └── VendorsPage.tsx
493
+ └── by-using-modal/ ← Pattern 2 (Form Modal)
494
+ ├── vendor-types.ts
495
+ ├── vendor-modal-form.tsx ← Uses ModalCard
496
+ ├── useVendorModalForm.tsx ← Uses useModalForm()
497
+ └── VendorModalPage.tsx
498
+ ```
499
+
500
+ For **Patterns 3 & 4** (Raw Wrappers), you have two options:
501
+
502
+ - **Option A (Inline):** Best for one-off panels used in a single page.
503
+ - **Option B (Domain Hook):** Best when the same custom panel is reused across multiple pages.
504
+
505
+ Both options follow the same two-file structure shown above, except they do **not** require a `vendor-types.ts` file.
506
+
507
+ See **`COMPONENTS.md`** for the complete prop reference for all four built-in card types.
508
+
509
+ ---
510
+
511
+ ## Keyboard Shortcuts
512
+
513
+ The following shortcuts are automatically available whenever a panel or modal is open:
514
+
515
+ | Shortcut | Action |
516
+ | :--------------------------- | :----------------------------------------- |
517
+ | `Esc` | Close the topmost panel or modal |
518
+ | `Ctrl + Enter` / `⌘ + Enter` | Submit the active form (calls `onSubmit`) |
519
+ | `Alt + ← / →` | Switch between tabs in a tabbed form panel |
520
+ | `Shift + ?` | Open the keyboard shortcuts help dialog |
521
+
522
+ ---
523
+
524
+ ## Advanced — Creating a Custom Card Type
525
+
526
+ The four built-in card types (`SidePanelCard`, `ModalCard`, `SidePanelRaw`, and `ModalRaw`) cover most use cases. If you need a completely different presentation—such as a **bottom drawer**, **fullscreen overlay**, **toast-style notification panel**, or any other custom container—you can register your own card type in four steps.
527
+
528
+ > **Note:** This requires modifying `PanelProvider.tsx` and `constants.ts` inside your project's installed `pejay-ui/panels/` directory.
529
+
530
+ ---
531
+
532
+ ### Step 1 — Register a New Type (`constants.ts`)
533
+
534
+ Add a new entry to the `APP_PROVIDER_TYPE` constant:
535
+
536
+ ```ts
537
+ // src/pejay-ui/panels/core/constants.ts
538
+ export const APP_PROVIDER_TYPE = {
539
+ MODAL: "modal",
540
+ SIDE_PANEL: "side-panel",
541
+ MODAL_RAW: "modal-raw",
542
+ SIDE_PANEL_RAW: "side-panel-raw",
543
+ BOTTOM_DRAWER: "bottom-drawer", // Your custom type
544
+ } as const;
545
+ ```
546
+
547
+ ### Step 2 — Build your card component
548
+
549
+ Create your card however you like. The only contract is that it receives `children` and whatever extra props you need. There are no rules on layout:
550
+
551
+ ```tsx
552
+ // src/pejay-ui/panels/components/bottom-drawer/bottom-drawer-card.tsx
553
+ import type { ReactNode } from "react";
554
+
555
+ interface BottomDrawerCardProps {
556
+ children: ReactNode;
557
+ close: () => void;
558
+ title?: string;
559
+ }
560
+
561
+ export function BottomDrawerCard({
562
+ children,
563
+ close,
564
+ title,
565
+ }: BottomDrawerCardProps) {
566
+ return (
567
+ <div className="fixed bottom-0 left-0 right-0 bg-white rounded-t-2xl shadow-2xl p-6 max-h-[80vh] overflow-y-auto">
568
+ <div className="flex justify-between items-center mb-4">
569
+ {title && <h2 className="text-lg font-bold">{title}</h2>}
570
+ <button onClick={close} className="ml-auto">
571
+
572
+ </button>
573
+ </div>
574
+ {children}
575
+ </div>
576
+ );
577
+ }
578
+ ```
579
+
580
+ ---
581
+
582
+ ### Step 3 — Register it in `PanelProvider.tsx`
583
+
584
+ Import your new card and add a new `if` block inside the `stack.map()` renderer:
585
+
586
+ ```tsx
587
+ // src/pejay-ui/panels/core/PanelProvider.tsx
588
+ import { BottomDrawerCard } from "../components/bottom-drawer/bottom-drawer-card";
589
+ import { Backdrop } from "../components/modal/backdrop"; // reuse existing backdrop
590
+
591
+ // Inside stack.map((item, index) => { ... }):
592
+
593
+ /* ── Bottom Drawer ── */
594
+ if (item.type === APP_PROVIDER_TYPE.BOTTOM_DRAWER) {
595
+ const closeId = () => close(item.id);
596
+ return (
597
+ <div
598
+ key={item.id}
599
+ style={{ position: "fixed", inset: 0, zIndex: getOverlayContentZ(layer) }}
600
+ >
601
+ <Backdrop onClick={closeId} layer={layer} />
602
+ <BottomDrawerCard close={closeId} title={item.options?.title as string}>
603
+ {item.content?.({ close: closeId })}
604
+ </BottomDrawerCard>
605
+ </div>
606
+ );
607
+ }
608
+ ```
609
+
610
+ > **Tip:** You can reuse the existing `<Backdrop>`, `<SidePanel>`, or `<Modal>` shell components as outer containers and only swap out the inner card — this gives you the backdrop + animation for free.
611
+
612
+ ---
613
+
614
+ ### Step 4 — Create an `openBottomDrawer` hook
615
+
616
+ Use `usePanelOverlayActions()` to access the raw `open` primitive and wrap it in a clean hook:
617
+
618
+ ```tsx
619
+ // src/pejay-ui/panels/hooks/useBottomDrawer.tsx
620
+ import { useCallback } from "react";
621
+ import { usePanelOverlayActions } from "../core/panel-context";
622
+ import { APP_PROVIDER_TYPE } from "../core/constants";
623
+ import type { OverlayContent } from "../core/types";
624
+
625
+ export function useBottomDrawer() {
626
+ const { open } = usePanelOverlayActions();
627
+
628
+ return useCallback(
629
+ (content: OverlayContent, title?: string) => {
630
+ open(APP_PROVIDER_TYPE.BOTTOM_DRAWER, content, { title });
631
+ },
632
+ [open],
633
+ );
634
+ }
635
+ ```
636
+
637
+ ---
638
+
639
+ ### Using it (same domain hook pattern as all other types)
640
+
641
+ **Option A — Inline:**
642
+
643
+ ```tsx
644
+ import { useBottomDrawer } from "@/pejay-ui/panels/hooks/useBottomDrawer";
645
+
646
+ function MyPage() {
647
+ const openDrawer = useBottomDrawer();
648
+
649
+ return (
650
+ <button
651
+ onClick={() =>
652
+ openDrawer(
653
+ ({ close }) => (
654
+ <div>
655
+ <p>Bottom drawer content</p>
656
+ <button onClick={close}>Close</button>
657
+ </div>
658
+ ),
659
+ "My Drawer Title",
660
+ )
661
+ }
662
+ >
663
+ Open Drawer
664
+ </button>
665
+ );
666
+ }
667
+ ```
668
+
669
+ **Option B — Domain hook for reuse:**
670
+
671
+ ```tsx
672
+ // useMyDrawer.tsx
673
+ import { useCallback } from "react";
674
+ import { useBottomDrawer } from "@/pejay-ui/panels/hooks/useBottomDrawer";
675
+ import { MyDrawerContent } from "./my-drawer-content";
676
+
677
+ export function useMyDrawer() {
678
+ const openDrawer = useBottomDrawer();
679
+
680
+ return useCallback(
681
+ (data?: MyData) => {
682
+ openDrawer(
683
+ ({ close }) => <MyDrawerContent close={close} data={data} />,
684
+ "My Drawer",
685
+ );
686
+ },
687
+ [openDrawer],
688
+ );
689
+ }
690
+ ```
691
+
692
+ ```tsx
693
+ // Any page
694
+ import { useMyDrawer } from "./useMyDrawer";
695
+
696
+ function PageA() {
697
+ const openDrawer = useMyDrawer();
698
+ return <button onClick={() => openDrawer(rowData)}>Open Drawer</button>;
699
+ }
700
+ ```
701
+
702
+ > **Important:** Your custom hook (`useBottomDrawer`) lives inside your project's `pejay-ui/panels/hooks/` folder, not in the `@/pejay-ui/panels` barrel export by default. Either import it directly from its file path, or add it to `hooks/index.ts` so it's available from the main `@/pejay-ui/panels` import.