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,112 @@
1
+ import { useCallback, useState } from "react";
2
+ import { ModalCard } from "../../components/modal/modal-card";
3
+ import { useFormDirty } from "../../hooks/useFormDirty";
4
+ import { requestOverlayCloseWithConfirm } from "../../core/overlay-close";
5
+ import type { Vendor } from "./vendor-types";
6
+ import {
7
+ getDefaultVendorFormValues,
8
+ vendorToFormValues,
9
+ type VendorFormValues,
10
+ } from "./vendor-types";
11
+
12
+ export type VendorFormMode = "create" | "update";
13
+
14
+ export type VendorModalFormProps = {
15
+ close: () => void;
16
+ mode?: VendorFormMode;
17
+ vendor?: Vendor;
18
+ };
19
+
20
+ export function VendorModalForm({
21
+ close,
22
+ mode = "create",
23
+ vendor,
24
+ }: VendorModalFormProps) {
25
+ const isUpdate = mode === "update";
26
+
27
+ const [values, setValues] = useState<VendorFormValues>(() => ({
28
+ ...getDefaultVendorFormValues(),
29
+ ...(vendor ? vendorToFormValues(vendor) : {}),
30
+ }));
31
+
32
+ const setField = useCallback(
33
+ <K extends keyof VendorFormValues>(key: K, value: VendorFormValues[K]) => {
34
+ setValues((prev) => ({ ...prev, [key]: value }));
35
+ },
36
+ [],
37
+ );
38
+
39
+ const isDirty = useFormDirty(values);
40
+
41
+ const handleSave = () => {
42
+ if (!values.vendorName.trim()) {
43
+ alert("Vendor name is required");
44
+ return;
45
+ }
46
+ console.log("Saving vendor:", values);
47
+ close();
48
+ };
49
+
50
+ return (
51
+ <ModalCard
52
+ title={isUpdate ? "Edit Vendor" : "Add Vendor"}
53
+ description={isUpdate ? "Update the vendor's details below." : "Fill in the new vendor's details."}
54
+ close={close}
55
+ onSubmit={handleSave}
56
+ isDirty={isDirty}
57
+ size="md"
58
+ footer={
59
+ <div className="flex justify-end gap-3">
60
+ <button
61
+ type="button"
62
+ onClick={requestOverlayCloseWithConfirm}
63
+ className="px-4 py-2 border border-slate-200 rounded text-sm hover:bg-slate-50 transition-colors cursor-pointer"
64
+ >
65
+ Cancel
66
+ </button>
67
+ <button
68
+ type="button"
69
+ onClick={handleSave}
70
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors cursor-pointer"
71
+ >
72
+ Save
73
+ </button>
74
+ </div>
75
+ }
76
+ >
77
+ <div className="space-y-4">
78
+ <div className="flex flex-col gap-1.5">
79
+ <label className="text-xs font-semibold text-slate-500">
80
+ Vendor Name <span className="text-red-500">*</span>
81
+ </label>
82
+ <input
83
+ type="text"
84
+ value={values.vendorName}
85
+ onChange={(e) => setField("vendorName", e.target.value)}
86
+ className="w-full px-3 py-2 border border-slate-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
87
+ />
88
+ </div>
89
+
90
+ <div className="flex flex-col gap-1.5">
91
+ <label className="text-xs font-semibold text-slate-500">Email</label>
92
+ <input
93
+ type="email"
94
+ value={values.vendorEmail}
95
+ onChange={(e) => setField("vendorEmail", e.target.value)}
96
+ className="w-full px-3 py-2 border border-slate-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
97
+ />
98
+ </div>
99
+
100
+ <div className="flex flex-col gap-1.5">
101
+ <label className="text-xs font-semibold text-slate-500">Phone</label>
102
+ <input
103
+ type="tel"
104
+ value={values.vendorPhone}
105
+ onChange={(e) => setField("vendorPhone", e.target.value)}
106
+ className="w-full px-3 py-2 border border-slate-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
107
+ />
108
+ </div>
109
+ </div>
110
+ </ModalCard>
111
+ );
112
+ }
@@ -0,0 +1,29 @@
1
+ export type Vendor = {
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ phone: string;
6
+ status: "active" | "inactive";
7
+ };
8
+
9
+ export type VendorFormValues = {
10
+ vendorName: string;
11
+ vendorEmail: string;
12
+ vendorPhone: string;
13
+ };
14
+
15
+ export function getDefaultVendorFormValues(): VendorFormValues {
16
+ return {
17
+ vendorName: "",
18
+ vendorEmail: "",
19
+ vendorPhone: "",
20
+ };
21
+ }
22
+
23
+ export function vendorToFormValues(vendor: Vendor): VendorFormValues {
24
+ return {
25
+ vendorName: vendor.name,
26
+ vendorEmail: vendor.email,
27
+ vendorPhone: vendor.phone,
28
+ };
29
+ }
@@ -0,0 +1,47 @@
1
+ import { useVendorFormPanel } from "./useVendorFormPanel";
2
+ import type { Vendor } from "./vendor-types";
3
+
4
+ export default function VendorsPage() {
5
+ const openVendorForm = useVendorFormPanel();
6
+
7
+ const handleCreate = () => {
8
+ openVendorForm("create"); // no data — empty form
9
+ };
10
+
11
+ const handleEdit = (vendor: Vendor) => {
12
+ openVendorForm("update", vendor); // passes row data into form
13
+ };
14
+
15
+ return (
16
+ <div className="p-6 max-w-xl mx-auto space-y-4">
17
+ <h1 className="text-xl font-bold text-slate-800">Vendors Directory (Demo)</h1>
18
+ <p className="text-sm text-slate-500">
19
+ This is a demo page showing how to trigger the side panel form using the domain hook.
20
+ </p>
21
+
22
+ <div className="flex gap-4">
23
+ <button
24
+ onClick={handleCreate}
25
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors"
26
+ >
27
+ Add Vendor
28
+ </button>
29
+
30
+ <button
31
+ onClick={() =>
32
+ handleEdit({
33
+ id: "V-001",
34
+ name: "Acme Supplies",
35
+ email: "acme@mail.com",
36
+ phone: "555-0100",
37
+ status: "active",
38
+ })
39
+ }
40
+ className="px-4 py-2 border border-slate-200 rounded text-sm hover:bg-slate-50 transition-colors"
41
+ >
42
+ Edit Acme Supplies
43
+ </button>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,15 @@
1
+ import { useCallback } from "react";
2
+ import { useFormPanel } from "../../hooks/useFormPanel";
3
+ import { VendorForm, type VendorFormMode } from "./vendor-form";
4
+ import type { Vendor } from "./vendor-types";
5
+
6
+ export function useVendorFormPanel() {
7
+ const openForm = useFormPanel();
8
+
9
+ return useCallback(
10
+ (mode: VendorFormMode = "create", vendor?: Vendor) => {
11
+ openForm(VendorForm, { mode, vendor });
12
+ },
13
+ [openForm],
14
+ );
15
+ }
@@ -0,0 +1,108 @@
1
+ import { useCallback, useState } from "react";
2
+ import { SidePanelCard } from "../../components/side-panel/side-panel-card";
3
+ import { useFormDirty } from "../../hooks/useFormDirty";
4
+ import { requestOverlayCloseWithConfirm } from "../../core/overlay-close";
5
+ import type { Vendor } from "./vendor-types";
6
+ import {
7
+ getDefaultVendorFormValues,
8
+ vendorToFormValues,
9
+ type VendorFormValues,
10
+ } from "./vendor-types";
11
+
12
+ export type VendorFormMode = "create" | "update";
13
+
14
+ export type VendorFormProps = {
15
+ close: () => void;
16
+ mode?: VendorFormMode;
17
+ vendor?: Vendor;
18
+ };
19
+
20
+ export function VendorForm({ close, mode = "create", vendor }: VendorFormProps) {
21
+ const isUpdate = mode === "update";
22
+
23
+ const [values, setValues] = useState<VendorFormValues>(() => ({
24
+ ...getDefaultVendorFormValues(),
25
+ ...(vendor ? vendorToFormValues(vendor) : {}),
26
+ }));
27
+
28
+ const setField = useCallback(
29
+ <K extends keyof VendorFormValues>(key: K, value: VendorFormValues[K]) => {
30
+ setValues((prev) => ({ ...prev, [key]: value }));
31
+ },
32
+ [],
33
+ );
34
+
35
+ const isDirty = useFormDirty(values);
36
+
37
+ const handleSave = () => {
38
+ if (!values.vendorName.trim()) {
39
+ alert("Vendor name is required");
40
+ return;
41
+ }
42
+
43
+ console.log("Saving vendor:", values);
44
+ close();
45
+ };
46
+
47
+ return (
48
+ <SidePanelCard
49
+ title={isUpdate ? "Edit Vendor" : "Add Vendor"}
50
+ close={close}
51
+ onSubmit={handleSave}
52
+ isDirty={isDirty}
53
+ size="md"
54
+ footer={
55
+ <div className="flex justify-end gap-3">
56
+ <button
57
+ type="button"
58
+ onClick={requestOverlayCloseWithConfirm}
59
+ className="px-4 py-2 border border-slate-200 rounded text-sm hover:bg-slate-50 transition-colors"
60
+ >
61
+ Cancel
62
+ </button>
63
+ <button
64
+ type="button"
65
+ onClick={handleSave}
66
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors"
67
+ >
68
+ Save
69
+ </button>
70
+ </div>
71
+ }
72
+ >
73
+ <div className="space-y-4 py-4">
74
+ <div className="flex flex-col gap-1.5">
75
+ <label className="text-xs font-semibold text-slate-500">
76
+ Vendor Name <span className="text-red-500">*</span>
77
+ </label>
78
+ <input
79
+ type="text"
80
+ value={values.vendorName}
81
+ onChange={(e) => setField("vendorName", e.target.value)}
82
+ className="w-full px-3 py-2 border border-slate-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
83
+ />
84
+ </div>
85
+
86
+ <div className="flex flex-col gap-1.5">
87
+ <label className="text-xs font-semibold text-slate-500">Email</label>
88
+ <input
89
+ type="email"
90
+ value={values.vendorEmail}
91
+ onChange={(e) => setField("vendorEmail", e.target.value)}
92
+ className="w-full px-3 py-2 border border-slate-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
93
+ />
94
+ </div>
95
+
96
+ <div className="flex flex-col gap-1.5">
97
+ <label className="text-xs font-semibold text-slate-500">Phone</label>
98
+ <input
99
+ type="tel"
100
+ value={values.vendorPhone}
101
+ onChange={(e) => setField("vendorPhone", e.target.value)}
102
+ className="w-full px-3 py-2 border border-slate-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
103
+ />
104
+ </div>
105
+ </div>
106
+ </SidePanelCard>
107
+ );
108
+ }
@@ -0,0 +1,29 @@
1
+ export type Vendor = {
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ phone: string;
6
+ status: "active" | "inactive";
7
+ };
8
+
9
+ export type VendorFormValues = {
10
+ vendorName: string;
11
+ vendorEmail: string;
12
+ vendorPhone: string;
13
+ };
14
+
15
+ export function getDefaultVendorFormValues(): VendorFormValues {
16
+ return {
17
+ vendorName: "",
18
+ vendorEmail: "",
19
+ vendorPhone: "",
20
+ };
21
+ }
22
+
23
+ export function vendorToFormValues(vendor: Vendor): VendorFormValues {
24
+ return {
25
+ vendorName: vendor.name,
26
+ vendorEmail: vendor.email,
27
+ vendorPhone: vendor.phone,
28
+ };
29
+ }
@@ -0,0 +1,62 @@
1
+ # Select Dropdown Components
2
+
3
+ A set of highly accessible single and multi-select dropdown fields with built-in search filtering, custom option templates, keyboard support, and async loading spinners.
4
+
5
+ ---
6
+
7
+ ## 1. Single Select Dropdown (`SelectInput`)
8
+
9
+ ```tsx
10
+ import { SelectInput } from "@/pejay-ui/components/select-dropdown";
11
+
12
+ const options = [
13
+ { label: "Option 1", value: "1" },
14
+ { label: "Option 2", value: "2" }
15
+ ];
16
+
17
+ <SelectInput
18
+ label="Select Project"
19
+ options={options}
20
+ value={value}
21
+ onChange={setValue}
22
+ searchable
23
+ placeholder="Choose a project..."
24
+ />
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 2. Multi-Select Dropdown (`MultiSelectInput`)
30
+
31
+ Supports selecting multiple items, rendering selected items as dismissible badges inside the input field.
32
+
33
+ ```tsx
34
+ import { MultiSelectInput } from "@/pejay-ui/components/select-dropdown";
35
+
36
+ <MultiSelectInput
37
+ label="Select Tags"
38
+ options={options}
39
+ value={values}
40
+ onChange={setValues}
41
+ searchable
42
+ maxSelected={5}
43
+ />
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 3. Component API Props
49
+
50
+ ### Shared Dropdown Props
51
+
52
+ | Prop | Type | Default | Description |
53
+ | :--- | :--- | :--- | :--- |
54
+ | `label` | `string` | — | Label text shown above the select field. |
55
+ | `options` | `Array<{ label: string; value: string; disabled?: boolean }>` | — | The option items list. |
56
+ | `placeholder` | `string` | `"Select..."` | Helper placeholder shown when empty. |
57
+ | `searchable` | `boolean` | `false` | Enables filter search input inside the dropdown. |
58
+ | `loading` | `boolean` | `false` | Shows a spinner overlay inside the list box. |
59
+ | `disabled` | `boolean` | `false` | Disables toggle dropdown interactions. |
60
+ | `error` | `string` | — | Highlights the input border in red with a text alert. |
61
+ | `renderOption` | `(option: Option) => ReactNode` | — | Optional custom renderer callback to style list options. |
62
+ | `renderValue` | `(selected: Option \| Option[]) => ReactNode` | — | Optional custom renderer callback to style selected values inside the header. |
@@ -9,8 +9,8 @@ import {
9
9
  FloatingPortal,
10
10
  } from "@floating-ui/react";
11
11
  import { ChevronDown, Check, X, Search, Loader2 } from "lucide-react";
12
- import { cn } from "@/utils/cn";
13
- import { Tooltip } from "../button/tooltip";
12
+ import { cn } from "@/pejay-ui/utils/cn";
13
+ import { Tooltip } from "@/pejay-ui/components/overlays";
14
14
  import { type SelectOption } from "./select-input";
15
15
 
16
16
  function getNextSelectableIndex(
@@ -9,8 +9,8 @@ import {
9
9
  FloatingPortal,
10
10
  } from "@floating-ui/react";
11
11
  import { ChevronDown, Check, Search, X, Loader2 } from "lucide-react";
12
- import { cn } from "@/utils/cn";
13
- import { Tooltip } from "../button/tooltip";
12
+ import { cn } from "@/pejay-ui/utils/cn";
13
+ import { Tooltip } from "@/pejay-ui/components/overlays";
14
14
 
15
15
  export interface SelectOption {
16
16
  id: string;
@@ -0,0 +1,53 @@
1
+ # Skeleton Shimmer Loader Component
2
+
3
+ A fluid skeleton component with built-in shimmery transitions to represent load state structures.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npx pejay-ui add skeleton
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Usage
16
+
17
+ ```tsx
18
+ import { Skeleton } from "@/pejay-ui/components/skeleton";
19
+
20
+ // 1. Circle skeleton placeholder (e.g. Profile Avatar)
21
+ <Skeleton variant="circle" width={48} height={48} />
22
+
23
+ // 2. Text line skeleton (default width 75%)
24
+ <Skeleton variant="text" />
25
+
26
+ // 3. Rectangular block (e.g. Card Preview container)
27
+ <Skeleton variant="rect" width="100%" height={200} />
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Component API Props
33
+
34
+ | Prop | Type | Default | Description |
35
+ | :--- | :--- | :--- | :--- |
36
+ | **`variant`** | `"text" \| "circle" \| "rect"` | `"rect"` | Mapped preset shape. |
37
+ | **`width`** | `string \| number` | — | Direct width style (e.g. `'100%'`, `'48px'`, or `48` for px value). |
38
+ | **`height`** | `string \| number` | — | Direct height style (e.g. `'16px'`, or `16` for px value). |
39
+ | **`className`** | `string` | — | Custom Tailwind class overrides. |
40
+
41
+ ---
42
+
43
+ ## Edge Cases & Best Practices
44
+
45
+ 1. **Accessibility (Screen Readers):**
46
+ * *Edge Case:* Screen readers will try to read empty skeleton elements or interpret their background animations as text contents.
47
+ * *Solution:* The Skeleton component includes `aria-hidden="true"` by default to ensure screen readers skip loading skeletons, maintaining screen reader accessibility clean.
48
+ 2. **Cumulative Layout Shift (CLS):**
49
+ * *Edge Case:* If the loaded content has different sizes or margins than the placeholder skeleton, elements on the screen will shift abruptly when loading finishes.
50
+ * *Solution:* Always match the `width` and `height` properties of your skeletons exactly to the sizes of the components they represent.
51
+ 3. **Responsive Fluid Widths:**
52
+ * *Edge Case:* Hardcoded pixel widths might overflow container boundaries on mobile devices.
53
+ * *Solution:* Pass responsive class names (e.g. `className="w-full md:w-48"`) instead of fixed `width` properties where layout widths need to adapt dynamically.
@@ -0,0 +1,2 @@
1
+ export * from "./skeleton";
2
+ export * from "./types";
@@ -0,0 +1,36 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ SKELETON SHIMMER KEYFRAMES
3
+ ───────────────────────────────────────────────────────────── */
4
+
5
+ @keyframes skeleton-shimmer {
6
+ 0% {
7
+ background-position: -200% 0;
8
+ }
9
+ 100% {
10
+ background-position: 200% 0;
11
+ }
12
+ }
13
+
14
+ .skeleton-shimmer-bg {
15
+ background: linear-gradient(
16
+ 90deg,
17
+ rgba(0, 0, 0, 0.05) 25%,
18
+ rgba(0, 0, 0, 0.10) 37%,
19
+ rgba(0, 0, 0, 0.05) 63%
20
+ );
21
+ background-size: 200% 100%;
22
+ animation: skeleton-shimmer 1.5s infinite linear;
23
+ }
24
+
25
+ /* Dark mode override */
26
+ .dark .skeleton-shimmer-bg,
27
+ @media (prefers-color-scheme: dark) {
28
+ .skeleton-shimmer-bg {
29
+ background: linear-gradient(
30
+ 90deg,
31
+ rgba(255, 255, 255, 0.05) 25%,
32
+ rgba(255, 255, 255, 0.10) 37%,
33
+ rgba(255, 255, 255, 0.05) 63%
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,40 @@
1
+ // @ts-ignore
2
+ import "./skeleton.css";
3
+ import type { SkeletonProps } from "./types";
4
+ import { cn } from "@/pejay-ui/utils/cn";
5
+
6
+ export function Skeleton({
7
+ variant = "rect",
8
+ width,
9
+ height,
10
+ className,
11
+ style,
12
+ ...props
13
+ }: SkeletonProps) {
14
+ // Shape configurations based on variant
15
+ const variantClass =
16
+ variant === "circle"
17
+ ? "rounded-full shrink-0"
18
+ : variant === "text"
19
+ ? "rounded h-[10px] w-3/4"
20
+ : "rounded-xl";
21
+
22
+ const resolvedStyle = {
23
+ width: typeof width === "number" ? `${width}px` : width,
24
+ height: typeof height === "number" ? `${height}px` : height,
25
+ ...style,
26
+ };
27
+
28
+ return (
29
+ <div
30
+ aria-hidden="true"
31
+ className={cn(
32
+ "skeleton-shimmer-bg select-none pointer-events-none border border-zinc-900/10",
33
+ variantClass,
34
+ className
35
+ )}
36
+ style={resolvedStyle}
37
+ {...props}
38
+ />
39
+ );
40
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ export type SkeletonVariant = "text" | "circle" | "rect";
4
+
5
+ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ /** Visual preset shape. Default: "rect" */
7
+ variant?: SkeletonVariant;
8
+ /** Direct width string (e.g. '100%', '48px', or 48) */
9
+ width?: string | number;
10
+ /** Direct height string (e.g. '16px', or 16) */
11
+ height?: string | number;
12
+ }
@@ -0,0 +1,51 @@
1
+ # Spinner Loading Component
2
+
3
+ A versatile suite of 9 distinct SVG and HTML loader animation styles with size scaling presets.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npx pejay-ui add spinner
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Usage
16
+
17
+ All variants dynamically inherit their color from their text color context using `currentColor` classes (so placing `<Spinner />` inside a primary button will automatically color it white/blue).
18
+
19
+ ```tsx
20
+ import { Spinner } from "@/pejay-ui/components/spinner";
21
+
22
+ // Classic loading ring (Default)
23
+ <Spinner variant="ring" size="md" />
24
+
25
+ // Three pulsing dots
26
+ <Spinner variant="dots" size="sm" />
27
+
28
+ // Dual circular ripple wave
29
+ <Spinner variant="ripple" size="lg" className="text-violet-600" />
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Component API Props
35
+
36
+ | Prop | Type | Default | Description |
37
+ | :--- | :--- | :--- | :--- |
38
+ | **`variant`** | `"ring" \| "dots" \| "pulse" \| "bars" \| "orbit" \| "ripple" \| "dots-ring" \| "dots-step" \| "text-dots"` | `"ring"` | The layout structure and animation keyframe preset style. |
39
+ | **`size`** | `"sm" \| "md" \| "lg"` | `"md"` | Standard scaling presets: `sm` (16px), `md` (24px), `lg` (36px). |
40
+ | **`className`** | `string` | — | Additional CSS classes for color inheritance or custom styling. |
41
+
42
+ ---
43
+
44
+ ## Edge Cases & Best Practices
45
+
46
+ 1. **Color Visibility:**
47
+ * *Edge Case:* Spinners inherit their color via `currentColor`. If placed inside an element with no text color defined on a dark or colored backdrop, the spinner may become invisible.
48
+ * *Solution:* Explicitly apply a text color class to the spinner or its parent container (e.g. `text-white` or `className="text-sky-500"`).
49
+ 2. **Layout Sizing & Shift:**
50
+ * *Edge Case:* Swapping layout text directly with a spinner can trigger layout changes if sizes are not fixed.
51
+ * *Solution:* Align spinners inside fixed-height flex containers (like the `h-9 inline-flex` button) to guarantee smooth, jitter-free loading transitions.
@@ -0,0 +1 @@
1
+ export * from "./spinner";