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.
- package/README.md +26 -0
- package/bin/cli.js +45 -15
- package/package.json +77 -54
- 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,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 "
|
|
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 "
|
|
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,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";
|