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,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,9 @@
1
+ let newRecordHandler: (() => void) | null = null;
2
+
3
+ export function setPageNewRecordHandler(handler: (() => void) | null) {
4
+ newRecordHandler = handler;
5
+ }
6
+
7
+ export function getPageNewRecordHandler() {
8
+ return newRecordHandler;
9
+ }
@@ -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,6 @@
1
+ import { useRef } from "react";
2
+
3
+ export function useFormDirty<T>(values: T): boolean {
4
+ const initialRef = useRef(values);
5
+ return JSON.stringify(values) !== JSON.stringify(initialRef.current);
6
+ }
@@ -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. |
@@ -13,7 +13,7 @@ import {
13
13
  FloatingPortal,
14
14
  type Placement,
15
15
  } from "@floating-ui/react";
16
- import { cn } from "@/utils/cn";
16
+ import { cn } from "@/pejay-ui/utils/cn";
17
17
 
18
18
  interface TooltipProps {
19
19
  children: React.ReactNode | string;
@@ -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.