@djangocfg/ui-core 2.1.570 → 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.570",
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.570",
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.570",
210
- "@djangocfg/i18n": "^2.1.570",
211
- "@djangocfg/typescript-config": "^2.1.570",
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 — accepts Date, string in any format, number (timestamp), or Moment */
35
- date: Date | string | number | moment.Moment;
36
- /** Input format hint if date is a string (e.g. "DD.MM.YYYY HH:mm"). Auto-detected if omitted. */
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
- /** Custom output format string (moment format). Used when format="custom" */
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
- function parseDate(
55
- value: Date | string | number | moment.Moment,
56
- inputFormat?: string,
57
- useUtc: boolean = true
58
- ): moment.Moment {
59
- if (moment.isMoment(value)) {
60
- return useUtc ? value.utc() : value.local();
61
- }
62
-
63
- const m = useUtc ? moment.utc : moment;
64
-
65
- if (value instanceof Date) {
66
- return m(value);
67
- }
68
-
69
- if (typeof value === "number") {
70
- return m(value);
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
- if (inputFormat) {
75
- return m(value, inputFormat, true);
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
- function getRelativeTime(date: moment.Moment, locale: string = "en"): string {
84
- moment.locale(locale);
85
- return date.fromNow();
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: moment.Moment, locale: string = "en"): string {
89
- moment.locale(locale);
90
- const now = moment();
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 date.format("HH:mm");
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 date.format("ddd");
125
+ return new Intl.DateTimeFormat(locale, { weekday: "short" }).format(date);
99
126
  }
100
127
  if (diffDays < 365) {
101
- return date.format("MMM D");
128
+ return new Intl.DateTimeFormat(locale, {
129
+ month: "short",
130
+ day: "numeric",
131
+ }).format(date);
102
132
  }
103
- return date.format("MMM D, YYYY");
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: moment.Moment, locale: string = "en"): string {
107
- moment.locale(locale);
108
- return date.format("MMM D, YYYY HH:mm");
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: moment.Moment,
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
- moment.locale(locale);
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
- const isoString = dateObj.toISOString();
171
- const title = dateObj.format("LLLL");
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
- const target = moment.utc(targetDate).local();
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 = target.diff(moment(), 'seconds');
81
+ const diff = Math.round((targetMs - Date.now()) / 1000);
71
82
  setCountdown(secondsToState(diff));
72
83
  };
73
84