@djangocfg/ui-core 2.1.571 → 2.1.572
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.572",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"check:contrast": "node scripts/check-preset-contrast.mjs"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
|
-
"@djangocfg/i18n": "^2.1.
|
|
133
|
+
"@djangocfg/i18n": "^2.1.572",
|
|
134
134
|
"consola": "^3.4.2",
|
|
135
135
|
"lucide-react": "^0.545.0",
|
|
136
136
|
"moment": "^2.30.1",
|
|
@@ -206,9 +206,9 @@
|
|
|
206
206
|
"vaul": "1.1.2"
|
|
207
207
|
},
|
|
208
208
|
"devDependencies": {
|
|
209
|
-
"@djangocfg/eslint-config": "^2.1.
|
|
210
|
-
"@djangocfg/i18n": "^2.1.
|
|
211
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
209
|
+
"@djangocfg/eslint-config": "^2.1.572",
|
|
210
|
+
"@djangocfg/i18n": "^2.1.572",
|
|
211
|
+
"@djangocfg/typescript-config": "^2.1.572",
|
|
212
212
|
"@storybook/react-vite": "^10.5.0",
|
|
213
213
|
"@types/node": "^24.13.3",
|
|
214
214
|
"@types/react": "19.2.15",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import moment from "moment";
|
|
5
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
5
|
import { cn } from "../../../lib/utils";
|
|
7
6
|
|
|
@@ -31,13 +30,16 @@ const relativeTimeCardVariants = cva(
|
|
|
31
30
|
interface RelativeTimeCardProps
|
|
32
31
|
extends React.HTMLAttributes<HTMLSpanElement>,
|
|
33
32
|
VariantProps<typeof relativeTimeCardVariants> {
|
|
34
|
-
/** Date value —
|
|
35
|
-
date:
|
|
36
|
-
/**
|
|
33
|
+
/** Date value — a `Date`, an ISO 8601 string, or a timestamp in milliseconds */
|
|
34
|
+
date: DateInput;
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated Ignored. Dates are parsed as ISO 8601; arbitrary input patterns
|
|
37
|
+
* went away with moment. Kept so existing call sites still type-check.
|
|
38
|
+
*/
|
|
37
39
|
inputFormat?: string;
|
|
38
40
|
/** Display format: relative | auto | datetime | custom */
|
|
39
41
|
format?: "relative" | "auto" | "datetime" | "custom";
|
|
40
|
-
/**
|
|
42
|
+
/** Output pattern for `format="custom"`. Supports `YYYY MM DD HH mm ss`. */
|
|
41
43
|
outputFormat?: string;
|
|
42
44
|
/** Update interval in ms for relative format. @default 60000 */
|
|
43
45
|
updateInterval?: number;
|
|
@@ -51,65 +53,117 @@ interface RelativeTimeCardProps
|
|
|
51
53
|
utc?: boolean;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
/* Built on `Intl`, not moment.
|
|
57
|
+
*
|
|
58
|
+
* moment bundles 137 locale files and this module was the last thing importing
|
|
59
|
+
* it here. Because the package entry is `export *` under a file-level
|
|
60
|
+
* `'use client'`, that cost reached every consumer of the barrel, not just the
|
|
61
|
+
* ones rendering a date: measured on the MLS Caribbean property pages
|
|
62
|
+
* 2026-10-17, two 805 KB chunks on every listing, for a component those pages
|
|
63
|
+
* never render.
|
|
64
|
+
*
|
|
65
|
+
* `Intl.RelativeTimeFormat` and `Intl.DateTimeFormat` are in every browser this
|
|
66
|
+
* package targets, and they localise from the `locale` prop rather than from a
|
|
67
|
+
* global `moment.locale()` call — which was shared mutable state, so two cards
|
|
68
|
+
* with different locales fought over it.
|
|
69
|
+
*
|
|
70
|
+
* Two deliberate narrowings, both of moment-only behaviour:
|
|
71
|
+
* - `inputFormat` no longer parses arbitrary patterns. `Date` accepts ISO
|
|
72
|
+
* 8601; anything else was already locale-dependent guesswork.
|
|
73
|
+
* - `outputFormat` accepts the moment-style tokens this component actually
|
|
74
|
+
* shipped with (`YYYY MM DD HH mm ss`), not the full moment grammar.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
type DateInput = Date | string | number;
|
|
78
|
+
|
|
79
|
+
/** A timestamp with no zone designator is UTC here, as the APIs emit it. */
|
|
80
|
+
function toDate(value: DateInput, useUtc: boolean): Date {
|
|
81
|
+
if (value instanceof Date) return value;
|
|
82
|
+
if (typeof value === "number") return new Date(value);
|
|
73
83
|
if (typeof value === "string") {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return m(value);
|
|
84
|
+
const iso =
|
|
85
|
+
useUtc && !/(?:Z|[+-]\d{2}:?\d{2})$/.test(value) ? `${value}Z` : value;
|
|
86
|
+
return new Date(iso);
|
|
78
87
|
}
|
|
79
|
-
|
|
80
|
-
return m();
|
|
88
|
+
return new Date();
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
const RELATIVE_UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
|
|
92
|
+
["year", 365 * 24 * 60 * 60],
|
|
93
|
+
["month", 30 * 24 * 60 * 60],
|
|
94
|
+
["day", 24 * 60 * 60],
|
|
95
|
+
["hour", 60 * 60],
|
|
96
|
+
["minute", 60],
|
|
97
|
+
["second", 1],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function getRelativeTime(date: Date, locale: string = "en"): string {
|
|
101
|
+
const seconds = Math.round((date.getTime() - Date.now()) / 1000);
|
|
102
|
+
const absolute = Math.abs(seconds);
|
|
103
|
+
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
104
|
+
|
|
105
|
+
for (const [unit, size] of RELATIVE_UNITS) {
|
|
106
|
+
if (absolute >= size || unit === "second") {
|
|
107
|
+
return formatter.format(Math.round(seconds / size), unit);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return formatter.format(0, "second");
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
function getAutoFormat(date:
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const diffHours = now.diff(date, "hours");
|
|
92
|
-
const diffDays = now.diff(date, "days");
|
|
113
|
+
function getAutoFormat(date: Date, locale: string = "en"): string {
|
|
114
|
+
const diffHours = (Date.now() - date.getTime()) / 3_600_000;
|
|
115
|
+
const diffDays = diffHours / 24;
|
|
93
116
|
|
|
94
117
|
if (diffHours < 24) {
|
|
95
|
-
return
|
|
118
|
+
return new Intl.DateTimeFormat(locale, {
|
|
119
|
+
hour: "2-digit",
|
|
120
|
+
minute: "2-digit",
|
|
121
|
+
hour12: false,
|
|
122
|
+
}).format(date);
|
|
96
123
|
}
|
|
97
124
|
if (diffDays < 7) {
|
|
98
|
-
return
|
|
125
|
+
return new Intl.DateTimeFormat(locale, { weekday: "short" }).format(date);
|
|
99
126
|
}
|
|
100
127
|
if (diffDays < 365) {
|
|
101
|
-
return
|
|
128
|
+
return new Intl.DateTimeFormat(locale, {
|
|
129
|
+
month: "short",
|
|
130
|
+
day: "numeric",
|
|
131
|
+
}).format(date);
|
|
102
132
|
}
|
|
103
|
-
return
|
|
133
|
+
return new Intl.DateTimeFormat(locale, {
|
|
134
|
+
year: "numeric",
|
|
135
|
+
month: "short",
|
|
136
|
+
day: "numeric",
|
|
137
|
+
}).format(date);
|
|
104
138
|
}
|
|
105
139
|
|
|
106
|
-
function getDateTimeFormat(date:
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
function getDateTimeFormat(date: Date, locale: string = "en"): string {
|
|
141
|
+
return new Intl.DateTimeFormat(locale, {
|
|
142
|
+
year: "numeric",
|
|
143
|
+
month: "short",
|
|
144
|
+
day: "numeric",
|
|
145
|
+
hour: "2-digit",
|
|
146
|
+
minute: "2-digit",
|
|
147
|
+
hour12: false,
|
|
148
|
+
}).format(date);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The moment-style tokens this component shipped with, and only those. */
|
|
152
|
+
function applyCustomFormat(date: Date, pattern: string): string {
|
|
153
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
154
|
+
const tokens: Record<string, string> = {
|
|
155
|
+
YYYY: String(date.getFullYear()),
|
|
156
|
+
MM: pad(date.getMonth() + 1),
|
|
157
|
+
DD: pad(date.getDate()),
|
|
158
|
+
HH: pad(date.getHours()),
|
|
159
|
+
mm: pad(date.getMinutes()),
|
|
160
|
+
ss: pad(date.getSeconds()),
|
|
161
|
+
};
|
|
162
|
+
return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => tokens[t] ?? t);
|
|
109
163
|
}
|
|
110
164
|
|
|
111
165
|
function formatDate(
|
|
112
|
-
date:
|
|
166
|
+
date: Date,
|
|
113
167
|
format: "relative" | "auto" | "datetime" | "custom",
|
|
114
168
|
locale: string,
|
|
115
169
|
outputFormat?: string
|
|
@@ -122,8 +176,7 @@ function formatDate(
|
|
|
122
176
|
case "datetime":
|
|
123
177
|
return getDateTimeFormat(date, locale);
|
|
124
178
|
case "custom":
|
|
125
|
-
|
|
126
|
-
return date.format(outputFormat || "YYYY-MM-DD HH:mm");
|
|
179
|
+
return applyCustomFormat(date, outputFormat || "YYYY-MM-DD HH:mm");
|
|
127
180
|
}
|
|
128
181
|
}
|
|
129
182
|
|
|
@@ -149,10 +202,7 @@ const RelativeTimeCard = React.forwardRef<
|
|
|
149
202
|
},
|
|
150
203
|
ref
|
|
151
204
|
) => {
|
|
152
|
-
const dateObj = React.useMemo(
|
|
153
|
-
() => parseDate(date, inputFormat, utc),
|
|
154
|
-
[date, inputFormat, utc]
|
|
155
|
-
);
|
|
205
|
+
const dateObj = React.useMemo(() => toDate(date, utc), [date, utc]);
|
|
156
206
|
|
|
157
207
|
const [display, setDisplay] = React.useState(() =>
|
|
158
208
|
formatDate(dateObj, format, locale, outputFormat)
|
|
@@ -167,8 +217,16 @@ const RelativeTimeCard = React.forwardRef<
|
|
|
167
217
|
return () => clearInterval(id);
|
|
168
218
|
}, [dateObj, format, locale, outputFormat, updateInterval]);
|
|
169
219
|
|
|
170
|
-
|
|
171
|
-
|
|
220
|
+
// An invalid date would throw from `toISOString`, where moment returned
|
|
221
|
+
// null and rendered "Invalid date". Both attributes are omitted instead.
|
|
222
|
+
const valid = !Number.isNaN(dateObj.getTime());
|
|
223
|
+
const isoString = valid ? dateObj.toISOString() : undefined;
|
|
224
|
+
const title = valid
|
|
225
|
+
? new Intl.DateTimeFormat(locale, {
|
|
226
|
+
dateStyle: "full",
|
|
227
|
+
timeStyle: "short",
|
|
228
|
+
}).format(dateObj)
|
|
229
|
+
: undefined;
|
|
172
230
|
|
|
173
231
|
return (
|
|
174
232
|
<time
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import moment from 'moment';
|
|
4
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
4
|
|
|
6
5
|
export interface CountdownState {
|
|
@@ -64,10 +63,22 @@ export const useCountdown = (targetDate: string | null): CountdownState => {
|
|
|
64
63
|
useEffect(() => {
|
|
65
64
|
if (!targetDate) return;
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
// Plain `Date` arithmetic, not moment: the hook only ever needed the gap in
|
|
67
|
+
// seconds, and the conversion moment did (`.utc().local()`) is what a Date
|
|
68
|
+
// already is internally — an absolute instant, rendered in local time only
|
|
69
|
+
// when formatted. Importing moment for this shipped its 137 locale files to
|
|
70
|
+
// every consumer of this package's barrel export.
|
|
71
|
+
//
|
|
72
|
+
// A timestamp with no zone designator means UTC here, so append the `Z`
|
|
73
|
+
// that `Date` would otherwise read as local time.
|
|
74
|
+
const iso = /(?:Z|[+-]\d{2}:?\d{2})$/.test(targetDate)
|
|
75
|
+
? targetDate
|
|
76
|
+
: `${targetDate}Z`;
|
|
77
|
+
const targetMs = new Date(iso).getTime();
|
|
78
|
+
if (Number.isNaN(targetMs)) return;
|
|
68
79
|
|
|
69
80
|
const tick = () => {
|
|
70
|
-
const diff =
|
|
81
|
+
const diff = Math.round((targetMs - Date.now()) / 1000);
|
|
71
82
|
setCountdown(secondsToState(diff));
|
|
72
83
|
};
|
|
73
84
|
|