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,58 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
SPINNER KEYFRAME ANIMATIONS
|
|
3
|
+
───────────────────────────────────────────────────────────── */
|
|
4
|
+
|
|
5
|
+
/* [1] ring — classic rotating arc */
|
|
6
|
+
@keyframes spinner-ring {
|
|
7
|
+
to { transform: rotate(360deg); }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* [2] dots — three dots fading in/out sequentially */
|
|
11
|
+
@keyframes spinner-dot-fade {
|
|
12
|
+
0%, 80%, 100% { opacity: 0.15; transform: scale(0.75); }
|
|
13
|
+
40% { opacity: 1; transform: scale(1); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* [3] pulse — single circle pulsing */
|
|
17
|
+
@keyframes spinner-pulse {
|
|
18
|
+
0%, 100% { opacity: 0.15; transform: scale(0.6); }
|
|
19
|
+
50% { opacity: 1; transform: scale(1); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* [4] bars — three vertical bars scaling */
|
|
23
|
+
@keyframes spinner-bar {
|
|
24
|
+
0%, 80%, 100% { transform: scaleY(0.4); opacity: 0.3; }
|
|
25
|
+
40% { transform: scaleY(1); opacity: 1; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* [5] orbit — a dot orbiting a static circle */
|
|
29
|
+
@keyframes spinner-orbit {
|
|
30
|
+
to { transform: rotate(360deg); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* [6] ripple — rings expanding and fading */
|
|
34
|
+
@keyframes spinner-ripple {
|
|
35
|
+
0% { transform: scale(0); opacity: 0.8; }
|
|
36
|
+
100% { transform: scale(1); opacity: 0; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* [7] dots-ring — circular dots fading sequentially */
|
|
40
|
+
@keyframes spinner-dots-ring-fade {
|
|
41
|
+
0%, 100% { opacity: 0.2; transform: scale(0.8); }
|
|
42
|
+
50% { opacity: 1; transform: scale(1); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* [8] dots-step — sequential building dots */
|
|
46
|
+
@keyframes spinner-dots-step-one {
|
|
47
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@keyframes spinner-dots-step-two {
|
|
51
|
+
0%, 15%, 85%, 100% { opacity: 0; transform: scale(0); }
|
|
52
|
+
20%, 80% { opacity: 1; transform: scale(1); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@keyframes spinner-dots-step-three {
|
|
56
|
+
0%, 35%, 65%, 100% { opacity: 0; transform: scale(0); }
|
|
57
|
+
40%, 60% { opacity: 1; transform: scale(1); }
|
|
58
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import "./spinner.css";
|
|
3
|
+
|
|
4
|
+
export type SpinnerVariant =
|
|
5
|
+
| "ring"
|
|
6
|
+
| "dots"
|
|
7
|
+
| "pulse"
|
|
8
|
+
| "bars"
|
|
9
|
+
| "orbit"
|
|
10
|
+
| "ripple"
|
|
11
|
+
| "dots-ring"
|
|
12
|
+
| "dots-step"
|
|
13
|
+
| "text-dots";
|
|
14
|
+
|
|
15
|
+
export type SpinnerSize = "sm" | "md" | "lg";
|
|
16
|
+
|
|
17
|
+
export interface SpinnerProps {
|
|
18
|
+
variant?: SpinnerVariant;
|
|
19
|
+
size?: SpinnerSize;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sizeMap: Record<SpinnerSize, number> = {
|
|
24
|
+
sm: 16,
|
|
25
|
+
md: 24,
|
|
26
|
+
lg: 36,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function Spinner({ variant = "ring", size = "md", className }: SpinnerProps) {
|
|
30
|
+
const px = sizeMap[size];
|
|
31
|
+
|
|
32
|
+
/* ── [1] Ring ──────────────────────────────────────────────── */
|
|
33
|
+
if (variant === "ring") {
|
|
34
|
+
return (
|
|
35
|
+
<svg
|
|
36
|
+
width={px}
|
|
37
|
+
height={px}
|
|
38
|
+
viewBox="0 0 24 24"
|
|
39
|
+
fill="none"
|
|
40
|
+
className={className}
|
|
41
|
+
style={{ animation: "spinner-ring 0.8s linear infinite" }}
|
|
42
|
+
>
|
|
43
|
+
<circle cx="12" cy="12" r="9" stroke="rgba(255,255,255,0.12)" strokeWidth="2.5" />
|
|
44
|
+
<circle
|
|
45
|
+
cx="12"
|
|
46
|
+
cy="12"
|
|
47
|
+
r="9"
|
|
48
|
+
stroke="currentColor"
|
|
49
|
+
strokeWidth="2.5"
|
|
50
|
+
strokeLinecap="round"
|
|
51
|
+
strokeDasharray="30 57"
|
|
52
|
+
/>
|
|
53
|
+
</svg>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ── [2] Dots ──────────────────────────────────────────────── */
|
|
58
|
+
if (variant === "dots") {
|
|
59
|
+
const dotPx = px * 0.22;
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={className}
|
|
63
|
+
style={{ display: "flex", gap: px * 0.22, alignItems: "center" }}
|
|
64
|
+
>
|
|
65
|
+
{[0, 1, 2].map((i) => (
|
|
66
|
+
<div
|
|
67
|
+
key={i}
|
|
68
|
+
style={{
|
|
69
|
+
width: dotPx,
|
|
70
|
+
height: dotPx,
|
|
71
|
+
borderRadius: "50%",
|
|
72
|
+
background: "currentColor",
|
|
73
|
+
animation: `spinner-dot-fade 1.2s ease-in-out ${i * 0.2}s infinite`,
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── [3] Pulse ─────────────────────────────────────────────── */
|
|
82
|
+
if (variant === "pulse") {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={className}
|
|
86
|
+
style={{
|
|
87
|
+
width: px,
|
|
88
|
+
height: px,
|
|
89
|
+
borderRadius: "50%",
|
|
90
|
+
background: "currentColor",
|
|
91
|
+
animation: "spinner-pulse 1s ease-in-out infinite",
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── [4] Bars ──────────────────────────────────────────────── */
|
|
98
|
+
if (variant === "bars") {
|
|
99
|
+
const barW = Math.max(2, px * 0.14);
|
|
100
|
+
const barH = px * 0.7;
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className={className}
|
|
104
|
+
style={{ display: "flex", gap: barW * 0.9, alignItems: "center", height: px }}
|
|
105
|
+
>
|
|
106
|
+
{[0, 1, 2].map((i) => (
|
|
107
|
+
<div
|
|
108
|
+
key={i}
|
|
109
|
+
style={{
|
|
110
|
+
width: barW,
|
|
111
|
+
height: barH,
|
|
112
|
+
borderRadius: barW,
|
|
113
|
+
background: "currentColor",
|
|
114
|
+
transformOrigin: "center",
|
|
115
|
+
animation: `spinner-bar 1s ease-in-out ${i * 0.15}s infinite`,
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── [5] Orbit ─────────────────────────────────────────────── */
|
|
124
|
+
if (variant === "orbit") {
|
|
125
|
+
const r = px / 2;
|
|
126
|
+
const orbitR = r * 0.62;
|
|
127
|
+
const dotR = r * 0.18;
|
|
128
|
+
return (
|
|
129
|
+
<svg width={px} height={px} viewBox={`0 0 ${px} ${px}`} fill="none" className={className}>
|
|
130
|
+
<circle cx={r} cy={r} r={orbitR} stroke="rgba(255,255,255,0.12)" strokeWidth="1.5" />
|
|
131
|
+
<g
|
|
132
|
+
style={{
|
|
133
|
+
transformOrigin: `${r}px ${r}px`,
|
|
134
|
+
animation: "spinner-orbit 1s linear infinite",
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<circle cx={r} cy={r - orbitR} r={dotR} fill="currentColor" />
|
|
138
|
+
</g>
|
|
139
|
+
</svg>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── [6] Ripple ────────────────────────────────────────────── */
|
|
144
|
+
if (variant === "ripple") {
|
|
145
|
+
const r = px / 2;
|
|
146
|
+
return (
|
|
147
|
+
<svg width={px} height={px} viewBox={`0 0 ${px} ${px}`} fill="none" className={className}>
|
|
148
|
+
{[0, 0.5].map((delay, i) => (
|
|
149
|
+
<circle
|
|
150
|
+
key={i}
|
|
151
|
+
cx={r}
|
|
152
|
+
cy={r}
|
|
153
|
+
r={r * 0.9}
|
|
154
|
+
stroke="currentColor"
|
|
155
|
+
strokeWidth="1.5"
|
|
156
|
+
style={{
|
|
157
|
+
transformOrigin: `${r}px ${r}px`,
|
|
158
|
+
animation: `spinner-ripple 1.4s ease-out ${delay}s infinite`,
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
</svg>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ── [7] Dots Ring ─────────────────────────────────────────── */
|
|
167
|
+
if (variant === "dots-ring") {
|
|
168
|
+
const dots = [
|
|
169
|
+
{ cx: 20, cy: 12 },
|
|
170
|
+
{ cx: 17.66, cy: 17.66 },
|
|
171
|
+
{ cx: 12, cy: 20 },
|
|
172
|
+
{ cx: 6.34, cy: 17.66 },
|
|
173
|
+
{ cx: 4, cy: 12 },
|
|
174
|
+
{ cx: 6.34, cy: 6.34 },
|
|
175
|
+
{ cx: 12, cy: 4 },
|
|
176
|
+
{ cx: 17.66, cy: 6.34 },
|
|
177
|
+
];
|
|
178
|
+
return (
|
|
179
|
+
<svg width={px} height={px} viewBox="0 0 24 24" fill="none" className={className}>
|
|
180
|
+
{dots.map((dot, i) => (
|
|
181
|
+
<circle
|
|
182
|
+
key={i}
|
|
183
|
+
cx={dot.cx}
|
|
184
|
+
cy={dot.cy}
|
|
185
|
+
r="2"
|
|
186
|
+
fill="currentColor"
|
|
187
|
+
style={{
|
|
188
|
+
transformOrigin: `${dot.cx}px ${dot.cy}px`,
|
|
189
|
+
animation: "spinner-dots-ring-fade 1.2s infinite ease-in-out",
|
|
190
|
+
animationDelay: `${i * 0.15 - 1.2}s`,
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
))}
|
|
194
|
+
</svg>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ── [8] Dots Step ─────────────────────────────────────────── */
|
|
199
|
+
if (variant === "dots-step") {
|
|
200
|
+
const dotPx = px * 0.22;
|
|
201
|
+
const anims = [
|
|
202
|
+
"spinner-dots-step-one",
|
|
203
|
+
"spinner-dots-step-two",
|
|
204
|
+
"spinner-dots-step-three",
|
|
205
|
+
];
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
className={className}
|
|
209
|
+
style={{ display: "flex", gap: px * 0.22, alignItems: "center" }}
|
|
210
|
+
>
|
|
211
|
+
{[0, 1, 2].map((i) => (
|
|
212
|
+
<div
|
|
213
|
+
key={i}
|
|
214
|
+
style={{
|
|
215
|
+
width: dotPx,
|
|
216
|
+
height: dotPx,
|
|
217
|
+
borderRadius: "50%",
|
|
218
|
+
background: "currentColor",
|
|
219
|
+
animation: `${anims[i]} 1.0s ease-in-out infinite`,
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
))}
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ── [9] Text Dots ─────────────────────────────────────────── */
|
|
228
|
+
if (variant === "text-dots") {
|
|
229
|
+
const anims = [
|
|
230
|
+
"spinner-dots-step-one",
|
|
231
|
+
"spinner-dots-step-two",
|
|
232
|
+
"spinner-dots-step-three",
|
|
233
|
+
];
|
|
234
|
+
return (
|
|
235
|
+
<span
|
|
236
|
+
className={className}
|
|
237
|
+
style={{
|
|
238
|
+
display: "inline-flex",
|
|
239
|
+
gap: "1px",
|
|
240
|
+
height: "fit-content",
|
|
241
|
+
verticalAlign: "baseline",
|
|
242
|
+
lineHeight: 1,
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{[0, 1, 2].map((i) => (
|
|
246
|
+
<span
|
|
247
|
+
key={i}
|
|
248
|
+
style={{
|
|
249
|
+
display: "inline-block",
|
|
250
|
+
color: "currentColor",
|
|
251
|
+
animation: `${anims[i]} 1.0s ease-in-out infinite`,
|
|
252
|
+
transformOrigin: "bottom center",
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
.
|
|
256
|
+
</span>
|
|
257
|
+
))}
|
|
258
|
+
</span>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
Info,
|
|
9
9
|
X,
|
|
10
10
|
} from "lucide-react";
|
|
11
|
-
import { cn } from "@/utils/cn";
|
|
12
|
-
import { Portal } from "
|
|
11
|
+
import { cn } from "@/pejay-ui/utils/cn";
|
|
12
|
+
import { Portal } from "@/pejay-ui/components/overlays";
|
|
13
13
|
|
|
14
14
|
// Dictionary mapping toast types to their respective Lucide icons and Tailwind styles
|
|
15
15
|
const TOAST_STYLES = {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
### Date Time Formatter (`date-time-formatter`)
|
|
2
|
+
|
|
3
|
+
- **Description**: Validates, parses, formats, and compares dates/times using `dayjs`. All formatting and standard comparisons are normalized to the browser/app timezone detected via `dayjs.tz.guess()`.
|
|
4
|
+
|
|
5
|
+
- **Exported Functions**:
|
|
6
|
+
|
|
7
|
+
- `setDateFormat({ input, mode, customformat })`
|
|
8
|
+
- Formats supported date inputs into:
|
|
9
|
+
- `DD/MM/YYYY` (`date`)
|
|
10
|
+
- `hh:mm A` (`time`)
|
|
11
|
+
- `DD/MM/YYYY hh:mm A` (`datetime`)
|
|
12
|
+
- Custom formats via `customformat`
|
|
13
|
+
- Returns `null` for invalid inputs.
|
|
14
|
+
|
|
15
|
+
- `compareDates({ input, compareTo, mode })`
|
|
16
|
+
- Compares two valid dates and returns:
|
|
17
|
+
```ts
|
|
18
|
+
{
|
|
19
|
+
isSame: boolean;
|
|
20
|
+
isBefore: boolean;
|
|
21
|
+
isAfter: boolean;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
- Supported comparison modes:
|
|
25
|
+
- `timestamp`
|
|
26
|
+
- `time`
|
|
27
|
+
- `utc-time`
|
|
28
|
+
- `day`
|
|
29
|
+
- `week`
|
|
30
|
+
- `month`
|
|
31
|
+
- `year`
|
|
32
|
+
- Returns `null` if either date is invalid.
|
|
33
|
+
|
|
34
|
+
- **Supported Inputs**
|
|
35
|
+
- `dayjs()`
|
|
36
|
+
- `new Date()`
|
|
37
|
+
- Unix timestamp (milliseconds)
|
|
38
|
+
- `YYYY-MM-DD`
|
|
39
|
+
- `YYYY-MM-DDTHH:mm`
|
|
40
|
+
- `YYYY-MM-DDTHH:mm:ss`
|
|
41
|
+
- `YYYY-MM-DDTHH:mm:ss.SSS`
|
|
42
|
+
- ISO strings with timezone offsets:
|
|
43
|
+
- `YYYY-MM-DDTHH:mm:ssZ`
|
|
44
|
+
- `YYYY-MM-DDTHH:mm:ss+05:30`
|
|
45
|
+
- `YYYY-MM-DDTHH:mm:ss-04:00`
|
|
46
|
+
|
|
47
|
+
- **Comparison Modes**
|
|
48
|
+
- `timestamp`
|
|
49
|
+
- Compares exact timestamps (milliseconds since epoch).
|
|
50
|
+
|
|
51
|
+
- `time`
|
|
52
|
+
- Compares local wall-clock time after timezone conversion.
|
|
53
|
+
- Ignores date portion.
|
|
54
|
+
|
|
55
|
+
- `utc-time`
|
|
56
|
+
- Compares UTC clock time directly.
|
|
57
|
+
- Ignores date portion.
|
|
58
|
+
|
|
59
|
+
- `day`, `week`, `month`, `year`
|
|
60
|
+
- Uses Day.js unit-based comparisons.
|
|
61
|
+
|
|
62
|
+
- **Timezone Behavior**
|
|
63
|
+
- Formatting is performed in the browser/app timezone (`dayjs.tz.guess()`).
|
|
64
|
+
- `time` compares local clock times after timezone normalization.
|
|
65
|
+
- `utc-time` compares UTC clock times without local timezone influence.
|
|
66
|
+
- ISO strings containing timezone offsets are automatically converted to the local timezone before formatting and standard comparisons.
|
|
67
|
+
|
|
68
|
+
- **Features**
|
|
69
|
+
- Strict input validation.
|
|
70
|
+
- Browser timezone normalization.
|
|
71
|
+
- Local and UTC time-only comparison modes.
|
|
72
|
+
- Custom date formatting support.
|
|
73
|
+
- Supports Dayjs, Date, Unix timestamps, and ISO date strings.
|
|
74
|
+
- Safe null handling for invalid inputs.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import dayjs, { Dayjs } from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc";
|
|
3
|
+
import timezone from "dayjs/plugin/timezone";
|
|
4
|
+
import customParseFormat from "dayjs/plugin/customParseFormat";
|
|
5
|
+
|
|
6
|
+
dayjs.extend(utc);
|
|
7
|
+
dayjs.extend(timezone);
|
|
8
|
+
dayjs.extend(customParseFormat);
|
|
9
|
+
|
|
10
|
+
/* ------------------------------------------------
|
|
11
|
+
? Types & Interfaces
|
|
12
|
+
------------------------------------------------*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Accepted inputs:
|
|
16
|
+
* - `dayjs()`
|
|
17
|
+
* - `new Date()`
|
|
18
|
+
* - unix ms number
|
|
19
|
+
* - `YYYY-MM-DD`
|
|
20
|
+
* - `YYYY-MM-DDTHH:mm`
|
|
21
|
+
* - `YYYY-MM-DDTHH:mm:ss`
|
|
22
|
+
* - `YYYY-MM-DDTHH:mm:ss.SSS`
|
|
23
|
+
* - the same ISO shapes with `Z` or `+05:30` / `-04:00`
|
|
24
|
+
*/
|
|
25
|
+
type ISODateInput = `${string}Z` | `${string}${"+" | "-"}${string}:${string}`;
|
|
26
|
+
type DateOnlyInput = `${number}-${number}-${number}`;
|
|
27
|
+
type LocalDateTimeInput =
|
|
28
|
+
| `${number}-${number}-${number}T${number}:${number}`
|
|
29
|
+
| `${number}-${number}-${number}T${number}:${number}:${number}`
|
|
30
|
+
| `${number}-${number}-${number}T${number}:${number}:${number}.${number}`;
|
|
31
|
+
|
|
32
|
+
type DateInput =
|
|
33
|
+
| Dayjs
|
|
34
|
+
| Date
|
|
35
|
+
| number
|
|
36
|
+
| ISODateInput
|
|
37
|
+
| DateOnlyInput
|
|
38
|
+
| LocalDateTimeInput;
|
|
39
|
+
|
|
40
|
+
type FormatMode = "date" | "time" | "datetime";
|
|
41
|
+
type CompareUnit =
|
|
42
|
+
| "timestamp"
|
|
43
|
+
| "time"
|
|
44
|
+
| "utc-time"
|
|
45
|
+
| "day"
|
|
46
|
+
| "week"
|
|
47
|
+
| "month"
|
|
48
|
+
| "year";
|
|
49
|
+
|
|
50
|
+
type CompareResult = {
|
|
51
|
+
isSame: boolean;
|
|
52
|
+
isBefore: boolean;
|
|
53
|
+
isAfter: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/* ------------------------------------------------
|
|
57
|
+
? Config — browser timezone (auto-detected at runtime)
|
|
58
|
+
------------------------------------------------*/
|
|
59
|
+
|
|
60
|
+
const LOCAL_TZ = dayjs.tz.guess();
|
|
61
|
+
|
|
62
|
+
const ISO_WITH_TIMEZONE =
|
|
63
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
64
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
65
|
+
const LOCAL_DATE_TIME =
|
|
66
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/;
|
|
67
|
+
|
|
68
|
+
function toLocalZone(date: Dayjs) {
|
|
69
|
+
return date.tz(LOCAL_TZ);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------
|
|
73
|
+
? Input validation & parsing
|
|
74
|
+
------------------------------------------------*/
|
|
75
|
+
|
|
76
|
+
export function isValidDateInput(input: unknown): input is DateInput {
|
|
77
|
+
if (input == null) return false;
|
|
78
|
+
|
|
79
|
+
if (dayjs.isDayjs(input)) {
|
|
80
|
+
return input.isValid();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (input instanceof Date) {
|
|
84
|
+
return !Number.isNaN(input.getTime());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof input === "number") {
|
|
88
|
+
return Number.isFinite(input) && Number.isInteger(input);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof input === "string") {
|
|
92
|
+
const value = input.trim();
|
|
93
|
+
return (
|
|
94
|
+
ISO_WITH_TIMEZONE.test(value) ||
|
|
95
|
+
DATE_ONLY.test(value) ||
|
|
96
|
+
LOCAL_DATE_TIME.test(value)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Parse accepted input into dayjs, normalized to the browser's detected timezone. */
|
|
104
|
+
function parseToLocal(input: DateInput): Dayjs | null {
|
|
105
|
+
if (!isValidDateInput(input)) return null;
|
|
106
|
+
|
|
107
|
+
if (dayjs.isDayjs(input)) {
|
|
108
|
+
return toLocalZone(input);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (input instanceof Date) {
|
|
112
|
+
return toLocalZone(dayjs(input));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (typeof input === "number") {
|
|
116
|
+
return toLocalZone(dayjs(input));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof input === "string") {
|
|
120
|
+
const value = input.trim();
|
|
121
|
+
|
|
122
|
+
if (ISO_WITH_TIMEZONE.test(value)) {
|
|
123
|
+
const parsed = dayjs(value);
|
|
124
|
+
return parsed.isValid() ? toLocalZone(parsed) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (DATE_ONLY.test(value)) {
|
|
128
|
+
const parsed = dayjs(value, "YYYY-MM-DD", true);
|
|
129
|
+
return parsed.isValid() ? toLocalZone(parsed) : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (LOCAL_DATE_TIME.test(value)) {
|
|
133
|
+
const parsed = dayjs(
|
|
134
|
+
value,
|
|
135
|
+
["YYYY-MM-DDTHH:mm", "YYYY-MM-DDTHH:mm:ss", "YYYY-MM-DDTHH:mm:ss.SSS"],
|
|
136
|
+
true,
|
|
137
|
+
);
|
|
138
|
+
return parsed.isValid() ? toLocalZone(parsed) : null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ------------------------------------------------
|
|
146
|
+
? Set date format
|
|
147
|
+
@param input - dayjs() | new Date() | ISO+Z/offset | unix ms
|
|
148
|
+
@param mode - Format mode (date, time, datetime)
|
|
149
|
+
@param customformat - Custom format string
|
|
150
|
+
@returns Formatted date string in the browser timezone
|
|
151
|
+
------------------------------------------------*/
|
|
152
|
+
|
|
153
|
+
export function setDateFormat({
|
|
154
|
+
input,
|
|
155
|
+
mode = "date",
|
|
156
|
+
customformat,
|
|
157
|
+
}: {
|
|
158
|
+
input: DateInput;
|
|
159
|
+
mode?: FormatMode;
|
|
160
|
+
customformat?: string;
|
|
161
|
+
}): string | null {
|
|
162
|
+
const date = parseToLocal(input);
|
|
163
|
+
if (!date) return null;
|
|
164
|
+
|
|
165
|
+
const formats = {
|
|
166
|
+
date: "DD/MM/YYYY",
|
|
167
|
+
time: "hh:mm A",
|
|
168
|
+
datetime: "DD/MM/YYYY hh:mm A",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return date.format(customformat ?? formats[mode]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ------------------------------------------------
|
|
175
|
+
? Compare dates
|
|
176
|
+
@param input - dayjs() | new Date() | ISO+Z/offset | unix ms
|
|
177
|
+
@param compareTo - dayjs() | new Date() | ISO+Z/offset | unix ms
|
|
178
|
+
@param mode - Comparison unit
|
|
179
|
+
@returns
|
|
180
|
+
{isSame: boolean;
|
|
181
|
+
isBefore: boolean;
|
|
182
|
+
isAfter: boolean;
|
|
183
|
+
}
|
|
184
|
+
------------------------------------------------*/
|
|
185
|
+
|
|
186
|
+
export function compareDates({
|
|
187
|
+
input,
|
|
188
|
+
compareTo,
|
|
189
|
+
mode,
|
|
190
|
+
}: {
|
|
191
|
+
input: DateInput;
|
|
192
|
+
compareTo: DateInput;
|
|
193
|
+
mode: CompareUnit;
|
|
194
|
+
}): CompareResult | null {
|
|
195
|
+
const left = parseToLocal(input);
|
|
196
|
+
const right = parseToLocal(compareTo);
|
|
197
|
+
|
|
198
|
+
if (!left || !right) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (mode === "timestamp") {
|
|
203
|
+
return {
|
|
204
|
+
isSame: left.valueOf() === right.valueOf(),
|
|
205
|
+
isBefore: left.valueOf() < right.valueOf(),
|
|
206
|
+
isAfter: left.valueOf() > right.valueOf(),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (mode === "time") {
|
|
211
|
+
const leftTime = getTimeValue(left);
|
|
212
|
+
const rightTime = getTimeValue(right);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
isSame: leftTime === rightTime,
|
|
216
|
+
isBefore: leftTime < rightTime,
|
|
217
|
+
isAfter: leftTime > rightTime,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (mode === "utc-time") {
|
|
222
|
+
const leftTime = getTimeValue(left.utc());
|
|
223
|
+
const rightTime = getTimeValue(right.utc());
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
isSame: leftTime === rightTime,
|
|
227
|
+
isBefore: leftTime < rightTime,
|
|
228
|
+
isAfter: leftTime > rightTime,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
isSame: left.isSame(right, mode),
|
|
234
|
+
isBefore: left.isBefore(right, mode),
|
|
235
|
+
isAfter: left.isAfter(right, mode),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ------------------------------------------------
|
|
240
|
+
? Helper Functions
|
|
241
|
+
------------------------------------------------*/
|
|
242
|
+
|
|
243
|
+
export function getTimeValue(date: Dayjs): number {
|
|
244
|
+
return (
|
|
245
|
+
date.hour() * 3600000 +
|
|
246
|
+
date.minute() * 60000 +
|
|
247
|
+
date.second() * 1000 +
|
|
248
|
+
date.millisecond()
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/*
|
|
253
|
+
Accepted inputs only:
|
|
254
|
+
dayjs()
|
|
255
|
+
new Date()
|
|
256
|
+
1718600000000
|
|
257
|
+
"2026-06-17"
|
|
258
|
+
"2026-06-17T10:30"
|
|
259
|
+
"2026-06-17T10:30:00"
|
|
260
|
+
"2026-06-17T10:30:00.123"
|
|
261
|
+
"2026-06-17T10:30:00Z"
|
|
262
|
+
"2026-06-17T10:30:00+05:30"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
Timezone:
|
|
266
|
+
- formatting and normal compare use dayjs.tz.guess() (browser/app timezone)
|
|
267
|
+
- `time` compares local wall-clock time
|
|
268
|
+
- `utc-time` compares UTC clock time
|
|
269
|
+
|
|
270
|
+
? Example Usage
|
|
271
|
+
|
|
272
|
+
setDateFormat({ input: dayjs() });
|
|
273
|
+
setDateFormat({ input: new Date(), mode: "datetime" });
|
|
274
|
+
setDateFormat({ input: "2026-06-17", mode: "date" });
|
|
275
|
+
setDateFormat({ input: "2026-06-17T10:30", mode: "datetime" });
|
|
276
|
+
setDateFormat({ input: "2026-06-17T10:30:00Z", mode: "time" });
|
|
277
|
+
setDateFormat({ input: 1718600000000, mode: "date" });
|
|
278
|
+
|
|
279
|
+
compareDates({
|
|
280
|
+
input: "2026-06-17T10:30:00Z",
|
|
281
|
+
compareTo: new Date(),
|
|
282
|
+
mode: "day",
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
compareDates({
|
|
286
|
+
input: "2026-06-17T10:30:00+05:30",
|
|
287
|
+
compareTo: "2026-06-17T10:30:00Z",
|
|
288
|
+
mode: "timestamp",
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
compareDates({
|
|
292
|
+
input: "2026-06-17T10:30:00Z",
|
|
293
|
+
compareTo: "2026-06-17T08:30:00Z",
|
|
294
|
+
mode: "utc-time",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// `time` compares local clock time after timezone conversion
|
|
298
|
+
compareDates({
|
|
299
|
+
input: "2026-06-17T10:30:00Z",
|
|
300
|
+
compareTo: "2026-06-17T10:30:00+05:30",
|
|
301
|
+
mode: "time",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// `utc-time` compares the UTC clock time directly
|
|
305
|
+
compareDates({
|
|
306
|
+
input: "2026-06-17T10:30:00Z",
|
|
307
|
+
compareTo: "2026-06-17T10:30:00+05:30",
|
|
308
|
+
mode: "utc-time",
|
|
309
|
+
});
|
|
310
|
+
*/
|