pejay-ui 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +26 -0
  2. package/bin/cli.js +45 -15
  3. package/package.json +2 -1
  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,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 "../overlays";
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
+ */