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,50 @@
|
|
|
1
|
+
import { notify } from "@/components/base/notify";
|
|
2
|
+
import {
|
|
3
|
+
getActiveFormOverlay,
|
|
4
|
+
getActiveOverlayClose,
|
|
5
|
+
isUnsavedConfirmOpen,
|
|
6
|
+
setUnsavedConfirmOpen,
|
|
7
|
+
} from "./form-overlay-registry";
|
|
8
|
+
import { UNSAVED_CHANGES_NOTIFY } from "./unsaved-changes-notify";
|
|
9
|
+
|
|
10
|
+
export { UNSAVED_CHANGES_NOTIFY } from "./unsaved-changes-notify";
|
|
11
|
+
|
|
12
|
+
export function showUnsavedChangesNotify(onDiscard: () => void) {
|
|
13
|
+
setUnsavedConfirmOpen(true);
|
|
14
|
+
|
|
15
|
+
notify.default({
|
|
16
|
+
title: UNSAVED_CHANGES_NOTIFY.title,
|
|
17
|
+
description: UNSAVED_CHANGES_NOTIFY.description,
|
|
18
|
+
onDismiss: () => setUnsavedConfirmOpen(false),
|
|
19
|
+
defaultButton: {
|
|
20
|
+
...UNSAVED_CHANGES_NOTIFY.defaultButton,
|
|
21
|
+
onClick: onDiscard,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function requestOverlayCloseWithConfirm() {
|
|
27
|
+
if (isUnsavedConfirmOpen()) {
|
|
28
|
+
notify.dismiss();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const form = getActiveFormOverlay();
|
|
33
|
+
if (form?.isCloseBlocked?.()) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const requestClose = getActiveOverlayClose();
|
|
38
|
+
const dirty = form?.isDirty?.() ?? false;
|
|
39
|
+
|
|
40
|
+
if (dirty && requestClose) {
|
|
41
|
+
showUnsavedChangesNotify(requestClose);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
requestClose?.();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function dismissActiveNotify() {
|
|
49
|
+
notify.dismiss();
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Edit copy and default button options here for the dirty-form close prompt. */
|
|
2
|
+
export const UNSAVED_CHANGES_NOTIFY = {
|
|
3
|
+
title: "You have unsaved changes",
|
|
4
|
+
description:
|
|
5
|
+
"Press Esc to keep editing, or discard changes to close without saving.",
|
|
6
|
+
defaultButton: {
|
|
7
|
+
label: "Discard changes",
|
|
8
|
+
autoFocus: true,
|
|
9
|
+
dismiss: true,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { KeyboardShortcutsHelp } from "@/components/global/keyboard-shortcuts-help";
|
|
3
|
+
import {
|
|
4
|
+
dismissActiveNotify,
|
|
5
|
+
requestOverlayCloseWithConfirm,
|
|
6
|
+
} from "@/hooks/overlay-close";
|
|
7
|
+
import {
|
|
8
|
+
getActiveFormOverlay,
|
|
9
|
+
isUnsavedConfirmOpen,
|
|
10
|
+
} from "@/hooks/form-overlay-registry";
|
|
11
|
+
import { getPageNewRecordHandler } from "@/hooks/page-shortcut-registry";
|
|
12
|
+
import { isModKey, isTypingTarget } from "@/hooks/keyboard-utils";
|
|
13
|
+
import type { Provider as Props } from "@/types";
|
|
14
|
+
|
|
15
|
+
type OverlayContent = Props.OverlayContent;
|
|
16
|
+
|
|
17
|
+
function navigateFormTab(direction: "prev" | "next") {
|
|
18
|
+
const tabs = getActiveFormOverlay()?.tabs;
|
|
19
|
+
if (!tabs || tabs.order.length === 0) return;
|
|
20
|
+
|
|
21
|
+
const currentIndex = tabs.order.indexOf(tabs.active);
|
|
22
|
+
if (currentIndex === -1) return;
|
|
23
|
+
|
|
24
|
+
const nextIndex =
|
|
25
|
+
direction === "next"
|
|
26
|
+
? (currentIndex + 1) % tabs.order.length
|
|
27
|
+
: (currentIndex - 1 + tabs.order.length) % tabs.order.length;
|
|
28
|
+
|
|
29
|
+
tabs.setActive(tabs.order[nextIndex]!);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useKeyboardShortcuts(
|
|
33
|
+
isOverlayOpen: boolean,
|
|
34
|
+
openModal: (content: OverlayContent) => void,
|
|
35
|
+
) {
|
|
36
|
+
const isOverlayOpenRef = useRef(isOverlayOpen);
|
|
37
|
+
isOverlayOpenRef.current = isOverlayOpen;
|
|
38
|
+
|
|
39
|
+
const openHelp = useCallback(() => {
|
|
40
|
+
openModal(({ close }) => <KeyboardShortcutsHelp close={close} />);
|
|
41
|
+
}, [openModal]);
|
|
42
|
+
|
|
43
|
+
const openHelpRef = useRef(openHelp);
|
|
44
|
+
openHelpRef.current = openHelp;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
48
|
+
const typing = isTypingTarget(event.target);
|
|
49
|
+
const mod = isModKey(event);
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
event.key === "?" &&
|
|
53
|
+
event.shiftKey &&
|
|
54
|
+
!event.ctrlKey &&
|
|
55
|
+
!event.metaKey &&
|
|
56
|
+
!event.altKey
|
|
57
|
+
) {
|
|
58
|
+
if (typing) return;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
openHelpRef.current();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isOverlayOpenRef.current) {
|
|
65
|
+
if (event.key === "Escape") {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
|
|
68
|
+
if (isUnsavedConfirmOpen()) {
|
|
69
|
+
dismissActiveNotify();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
requestOverlayCloseWithConfirm();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (mod && event.key === "Enter") {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
getActiveFormOverlay()?.onSubmit?.();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (event.altKey && event.key === "ArrowLeft") {
|
|
84
|
+
if (typing) return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
navigateFormTab("prev");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (event.altKey && event.key === "ArrowRight") {
|
|
91
|
+
if (typing) return;
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
navigateFormTab("next");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (mod && event.altKey && event.key.toLowerCase() === "n") {
|
|
101
|
+
if (typing) return;
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
getPageNewRecordHandler()?.();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
108
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
109
|
+
}, []);
|
|
110
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
registerActiveFormOverlay,
|
|
4
|
+
type FormOverlayRegistration,
|
|
5
|
+
type FormTabsConfig,
|
|
6
|
+
} from "./form-overlay-registry";
|
|
7
|
+
|
|
8
|
+
type UseFormOverlayRegistrationArgs = {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
onSubmit?: () => void;
|
|
11
|
+
isDirty?: boolean;
|
|
12
|
+
closeDisabled?: boolean;
|
|
13
|
+
formTabs?: FormTabsConfig;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useFormOverlayRegistration({
|
|
17
|
+
enabled = true,
|
|
18
|
+
onSubmit,
|
|
19
|
+
isDirty = false,
|
|
20
|
+
closeDisabled = false,
|
|
21
|
+
formTabs,
|
|
22
|
+
}: UseFormOverlayRegistrationArgs) {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!enabled) {
|
|
25
|
+
registerActiveFormOverlay(null);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const registration: FormOverlayRegistration = {
|
|
30
|
+
onSubmit,
|
|
31
|
+
isDirty: () => isDirty,
|
|
32
|
+
isCloseBlocked: () => closeDisabled,
|
|
33
|
+
tabs: formTabs,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
registerActiveFormOverlay(registration);
|
|
37
|
+
return () => registerActiveFormOverlay(null);
|
|
38
|
+
}, [
|
|
39
|
+
enabled,
|
|
40
|
+
onSubmit,
|
|
41
|
+
isDirty,
|
|
42
|
+
closeDisabled,
|
|
43
|
+
formTabs?.active,
|
|
44
|
+
formTabs?.order.join(","),
|
|
45
|
+
formTabs?.setActive,
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useCallback, type ComponentType } from "react";
|
|
2
|
+
import { useOverlay } from "./useOverlay";
|
|
3
|
+
|
|
4
|
+
export function useFormPanel() {
|
|
5
|
+
const { openSidePanel } = useOverlay();
|
|
6
|
+
|
|
7
|
+
return useCallback(
|
|
8
|
+
<P extends { close: () => void }>(
|
|
9
|
+
Component: ComponentType<P>,
|
|
10
|
+
props: Omit<P, "close">,
|
|
11
|
+
) => {
|
|
12
|
+
openSidePanel(({ close }) => (
|
|
13
|
+
<Component {...({ ...props, close } as P)} />
|
|
14
|
+
));
|
|
15
|
+
},
|
|
16
|
+
[openSidePanel],
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useCallback, type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
import type { FormTabsConfig } from "./form-overlay-registry";
|
|
3
|
+
|
|
4
|
+
export function useFormTabHandler<T extends string>(
|
|
5
|
+
setActive: Dispatch<SetStateAction<T>>,
|
|
6
|
+
) {
|
|
7
|
+
return useCallback((tab: T) => {
|
|
8
|
+
setActive(tab);
|
|
9
|
+
}, [setActive]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function toFormTabsConfig<T extends string>(
|
|
13
|
+
active: T,
|
|
14
|
+
handleTabChange: (tab: T) => void,
|
|
15
|
+
order: T[],
|
|
16
|
+
): FormTabsConfig {
|
|
17
|
+
return {
|
|
18
|
+
active,
|
|
19
|
+
setActive: (id) => handleTabChange(id as T),
|
|
20
|
+
order,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export function useHorizontalWheelScroll<T extends HTMLElement>() {
|
|
4
|
+
const ref = useRef<T>(null);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const element = ref.current;
|
|
8
|
+
if (!element) return;
|
|
9
|
+
|
|
10
|
+
const onWheel = (event: WheelEvent) => {
|
|
11
|
+
const maxScrollLeft = element.scrollWidth - element.clientWidth;
|
|
12
|
+
if (maxScrollLeft <= 0) return;
|
|
13
|
+
|
|
14
|
+
// Preserve native horizontal trackpad / shift+wheel gestures.
|
|
15
|
+
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
|
|
16
|
+
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
event.stopPropagation();
|
|
19
|
+
element.scrollLeft += event.deltaY;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
element.addEventListener("wheel", onWheel, { passive: false, capture: true });
|
|
23
|
+
return () => element.removeEventListener("wheel", onWheel, { capture: true });
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return ref;
|
|
27
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useAppProvider } from "@/provider/app-context";
|
|
3
|
+
import { APP_PROVIDER_TYPE } from "@/utils";
|
|
4
|
+
import type { Provider } from "@/types";
|
|
5
|
+
|
|
6
|
+
type OverlayContent = Provider.OverlayContent;
|
|
7
|
+
type OverlayOptions = Provider.OverlayOptions;
|
|
8
|
+
|
|
9
|
+
export function useOverlay() {
|
|
10
|
+
const { open, isOverlayOpen, stackDepth } = useAppProvider();
|
|
11
|
+
|
|
12
|
+
const openSidePanel = useCallback(
|
|
13
|
+
(content: OverlayContent, options?: OverlayOptions) => {
|
|
14
|
+
open(APP_PROVIDER_TYPE.SIDE_PANEL, content, {
|
|
15
|
+
onSide: "right",
|
|
16
|
+
...options,
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
[open],
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const openModal = useCallback(
|
|
23
|
+
(content: OverlayContent, options?: OverlayOptions) => {
|
|
24
|
+
open(APP_PROVIDER_TYPE.MODAL, content, options);
|
|
25
|
+
},
|
|
26
|
+
[open],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return { openSidePanel, openModal, isOverlayOpen, stackDepth };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @deprecated Prefer `useOverlay` */
|
|
33
|
+
export function useSidePanel() {
|
|
34
|
+
return useOverlay();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @deprecated Prefer `useOverlay` */
|
|
38
|
+
export function useModal() {
|
|
39
|
+
const { openModal, isOverlayOpen, stackDepth } = useOverlay();
|
|
40
|
+
return { openModal, isOverlayOpen, stackDepth };
|
|
41
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from "./portal";
|
|
1
|
+
export * from "./portal/portal";
|
|
2
|
+
export * from "./tooltip/tooltip";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createPortal } from "react-dom";
|
|
2
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
interface PortalProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export function Portal({ children }: PortalProps) {
|
|
8
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const div = document.createElement("div");
|
|
12
|
+
div.id = "dynamic-portal";
|
|
13
|
+
document.body.appendChild(div);
|
|
14
|
+
setContainer(div);
|
|
15
|
+
|
|
16
|
+
return () => {
|
|
17
|
+
if (div.parentNode) {
|
|
18
|
+
document.body.removeChild(div);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
if (!container) return null;
|
|
24
|
+
|
|
25
|
+
return createPortal(children, container);
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Tooltip Overlay Component
|
|
2
|
+
|
|
3
|
+
A standalone utility wrapper to show visual popover info bubbles when hovering over any child element.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Tooltip } from "@/pejay-ui/components/overlays/tooltip";
|
|
11
|
+
|
|
12
|
+
<Tooltip content="Upload CSV data file" placement="bottom">
|
|
13
|
+
<button className="p-2 border rounded">Upload</button>
|
|
14
|
+
</Tooltip>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Props
|
|
20
|
+
|
|
21
|
+
| Prop | Type | Default | Description |
|
|
22
|
+
| :--- | :--- | :--- | :--- |
|
|
23
|
+
| `children` | `ReactNode` | — | The target element that triggers the tooltip on hover. |
|
|
24
|
+
| `content` | `string` | — | Text inside the tooltip bubble. |
|
|
25
|
+
| `placement` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Tooltip popover alignment relative to the trigger. |
|
|
26
|
+
| `className` | `string` | — | Additional CSS classes for custom visual overrides. |
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Panel Component Cards Reference
|
|
2
|
+
|
|
3
|
+
There are **4 card types** available depending on how much control you need over the overlay's layout.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Form Cards (Recommended for most use cases)
|
|
8
|
+
|
|
9
|
+
These cards include a built-in title row, X button, footer slot, and full keyboard shortcut support (`Ctrl+Enter` to submit, `Escape` to close with dirty guard, tab navigation via `Alt+Arrow`).
|
|
10
|
+
|
|
11
|
+
### `SidePanelCard`
|
|
12
|
+
A full-height side panel that slides in from the right (or left). Use for **create / edit forms**.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { SidePanelCard } from "@/pejay-ui/panels";
|
|
16
|
+
|
|
17
|
+
<SidePanelCard
|
|
18
|
+
title="Add Vendor"
|
|
19
|
+
close={close}
|
|
20
|
+
onSubmit={handleSave}
|
|
21
|
+
isDirty={isDirty}
|
|
22
|
+
size="md" // "sm" | "md" | "lg" | "xl"
|
|
23
|
+
footer={<SaveCancelButtons />}
|
|
24
|
+
headerSlot={<TabsRow />}
|
|
25
|
+
>
|
|
26
|
+
{/* your form fields */}
|
|
27
|
+
</SidePanelCard>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `ModalCard`
|
|
31
|
+
A centered modal dialog. Same feature set as `SidePanelCard`. Use for **confirmations, quick edits, or view dialogs**.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { ModalCard } from "@/pejay-ui/panels";
|
|
35
|
+
|
|
36
|
+
<ModalCard
|
|
37
|
+
title="Edit Vendor"
|
|
38
|
+
description="Update the vendor's details."
|
|
39
|
+
close={close}
|
|
40
|
+
onSubmit={handleSave}
|
|
41
|
+
isDirty={isDirty}
|
|
42
|
+
size="md" // "sm" | "md" | "lg" | "xl"
|
|
43
|
+
footer={<SaveCancelButtons />}
|
|
44
|
+
>
|
|
45
|
+
{/* your form fields */}
|
|
46
|
+
</ModalCard>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Prop | Type | Description |
|
|
50
|
+
|:---|:---|:---|
|
|
51
|
+
| `title` | `string` | Heading shown at the top |
|
|
52
|
+
| `description` | `string` | Subtitle below the title |
|
|
53
|
+
| `close` | `() => void` | Injected by the hook — always pass through |
|
|
54
|
+
| `onSubmit` | `() => void` | Enables `Ctrl+Enter` shortcut |
|
|
55
|
+
| `isDirty` | `boolean` | When `true`, blocks close with unsaved-changes guard |
|
|
56
|
+
| `closeDisabled` | `boolean` | Disables X and Escape (use during async saves) |
|
|
57
|
+
| `footer` | `ReactNode` | Bottom bar — typically Cancel + Save buttons |
|
|
58
|
+
| `headerSlot` | `ReactNode` | Renders below title (e.g. tab switcher row) |
|
|
59
|
+
| `size` | `"sm"\|"md"\|"lg"\|"xl"` | Controls width preset |
|
|
60
|
+
| `formTabs` | `FormTabsConfig` | Enables `Alt+Arrow` tab switching |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Raw Wrappers (For fully custom layouts)
|
|
65
|
+
|
|
66
|
+
These wrappers render **only** a container with `p-2` padding. No title, no X button, no footer. Your content receives a `{ close }` callback and is fully responsible for triggering its own close action.
|
|
67
|
+
|
|
68
|
+
Use raw wrappers when you need to build a completely custom overlay layout that doesn't fit the standard form card structure.
|
|
69
|
+
|
|
70
|
+
### `SidePanelRaw`
|
|
71
|
+
Opened via `openRawSidePanel` from `useOverlay`.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useOverlay } from "@/pejay-ui/panels";
|
|
75
|
+
|
|
76
|
+
const { openRawSidePanel } = useOverlay();
|
|
77
|
+
|
|
78
|
+
openRawSidePanel(({ close }) => (
|
|
79
|
+
<div>
|
|
80
|
+
<p>Fully custom side panel content.</p>
|
|
81
|
+
<button onClick={close}>Dismiss</button>
|
|
82
|
+
</div>
|
|
83
|
+
));
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `ModalRaw`
|
|
87
|
+
Opened via `openRawModal` from `useOverlay`.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { useOverlay } from "@/pejay-ui/panels";
|
|
91
|
+
|
|
92
|
+
const { openRawModal } = useOverlay();
|
|
93
|
+
|
|
94
|
+
openRawModal(({ close }) => (
|
|
95
|
+
<div>
|
|
96
|
+
<p>Fully custom modal content.</p>
|
|
97
|
+
<button onClick={close}>Dismiss</button>
|
|
98
|
+
</div>
|
|
99
|
+
));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> [!TIP]
|
|
103
|
+
> Raw wrappers still benefit from the global `PanelProvider` stack — they support overlay stacking and the `Escape` key still closes them.
|