@crisp-ui-kit/crisp 0.33.2 → 0.33.3
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/dist/cjs/components/datatable/datatable.js +50 -21
- package/dist/cjs/core/cell-date.js +37 -0
- package/dist/cjs/core/index.js +4 -1
- package/dist/cjs/product/recordstable/recordstable.js +12 -21
- package/dist/esm/components/datatable/datatable.js +51 -22
- package/dist/esm/core/cell-date.js +33 -0
- package/dist/esm/core/index.js +1 -0
- package/dist/esm/product/recordstable/recordstable.js +8 -16
- package/dist/types/components/datatable/datatable.d.ts +7 -1
- package/dist/types/core/cell-date.d.ts +17 -0
- package/dist/types/core/index.d.ts +1 -0
- package/dist/types/product/recordstable/recordstable.d.ts +13 -4
- package/package.json +1 -1
|
@@ -46,12 +46,15 @@ const datatable_recipe_1 = require("./datatable.recipe");
|
|
|
46
46
|
* a display string. Returns "" when it cannot be parsed, so the field opens
|
|
47
47
|
* empty rather than showing an invalid value. */
|
|
48
48
|
function toDateInputValue(v) {
|
|
49
|
+
// formatCellDate, not toISOString().slice(0, 10): the latter takes the UTC
|
|
50
|
+
// day of a local Date, so a value picked late in the evening east of UTC
|
|
51
|
+
// round-trips as the next day (#221).
|
|
49
52
|
if (v instanceof Date && !Number.isNaN(v.getTime()))
|
|
50
|
-
return
|
|
53
|
+
return (0, core_1.formatCellDate)(v);
|
|
51
54
|
if (typeof v === "string" || typeof v === "number") {
|
|
52
|
-
const d =
|
|
53
|
-
if (
|
|
54
|
-
return
|
|
55
|
+
const d = (0, core_1.parseCellDate)(String(v));
|
|
56
|
+
if (d)
|
|
57
|
+
return (0, core_1.formatCellDate)(d);
|
|
55
58
|
}
|
|
56
59
|
return "";
|
|
57
60
|
}
|
|
@@ -89,21 +92,34 @@ function initials(name) {
|
|
|
89
92
|
const p = name.trim().split(/\s+/);
|
|
90
93
|
return ((p[0]?.[0] ?? "") + (p[1]?.[0] ?? "")).toUpperCase() || "?";
|
|
91
94
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// One Intl.DateTimeFormat per locale, built lazily and reused — constructing
|
|
96
|
+
// a formatter per cell render would be wasteful on a large grid.
|
|
97
|
+
const DATE_FMT_CACHE = new Map();
|
|
98
|
+
function dateFmt(locale) {
|
|
99
|
+
let fmt = DATE_FMT_CACHE.get(locale);
|
|
100
|
+
if (!fmt) {
|
|
101
|
+
fmt = new Intl.DateTimeFormat(locale, {
|
|
102
|
+
month: "short",
|
|
103
|
+
day: "numeric",
|
|
104
|
+
year: "numeric",
|
|
105
|
+
});
|
|
106
|
+
DATE_FMT_CACHE.set(locale, fmt);
|
|
107
|
+
}
|
|
108
|
+
return fmt;
|
|
109
|
+
}
|
|
110
|
+
function formatCell(type, v, symbol, locale = "en-US") {
|
|
98
111
|
if (type === "number")
|
|
99
|
-
return typeof v === "number" ? v.toLocaleString(
|
|
112
|
+
return typeof v === "number" ? v.toLocaleString(locale) : asText(v);
|
|
100
113
|
if (type === "currency")
|
|
101
114
|
return typeof v === "number"
|
|
102
|
-
? `${symbol ?? "$"}${v.toLocaleString(
|
|
115
|
+
? `${symbol ?? "$"}${v.toLocaleString(locale)}`
|
|
103
116
|
: asText(v);
|
|
104
117
|
if (type === "date") {
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
// parseCellDate, not `new Date(v)`: a date-only value is UTC midnight per
|
|
119
|
+
// ECMA-262, and formatting that in the reader's timezone renders it a day
|
|
120
|
+
// early west of UTC (#221, the same defect #220 fixed in RecordsTable).
|
|
121
|
+
const d = v ? (0, core_1.parseCellDate)(String(v)) : null;
|
|
122
|
+
return d ? dateFmt(locale).format(d) : asText(v);
|
|
107
123
|
}
|
|
108
124
|
return asText(v);
|
|
109
125
|
}
|
|
@@ -115,7 +131,7 @@ function formatCell(type, v, symbol) {
|
|
|
115
131
|
// silently drop the cell's data. Each branch decides for itself where a
|
|
116
132
|
// display string belongs -- as an entity's name, a status' label -- while the
|
|
117
133
|
// raw value keeps driving structure and tone.
|
|
118
|
-
function renderTyped(col, value, avatarTone, display) {
|
|
134
|
+
function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
119
135
|
const empty = (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-muted", children: "\u2014" });
|
|
120
136
|
switch (col.type) {
|
|
121
137
|
case "entity": {
|
|
@@ -146,7 +162,7 @@ function renderTyped(col, value, avatarTone, display) {
|
|
|
146
162
|
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
|
|
147
163
|
}
|
|
148
164
|
default: {
|
|
149
|
-
const text = display || formatCell(col.type, value, col.symbol);
|
|
165
|
+
const text = display || formatCell(col.type, value, col.symbol, locale);
|
|
150
166
|
const isNum = col.type === "number" || col.type === "currency";
|
|
151
167
|
if (!text)
|
|
152
168
|
return empty;
|
|
@@ -155,9 +171,16 @@ function renderTyped(col, value, avatarTone, display) {
|
|
|
155
171
|
}
|
|
156
172
|
}
|
|
157
173
|
const CHECK_W = 40;
|
|
158
|
-
function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
|
|
174
|
+
function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
|
|
159
175
|
? `${total} ${total === 1 ? "row" : "rows"}`
|
|
160
176
|
: `${shown} of ${total}`, toolbar, className, reorderRowLabel = "Reorder row", ...rest }) {
|
|
177
|
+
// Two different defaults on purpose. FORMATTING falls back to "en-US", which
|
|
178
|
+
// is what the built-in date/number/currency renderers have always produced,
|
|
179
|
+
// so no existing output moves. SORTING falls back to `undefined`, which is
|
|
180
|
+
// the runtime's own collation — passing "en-US" there would silently change
|
|
181
|
+
// the row order for every consumer on a non-en-US runtime, which is exactly
|
|
182
|
+
// the kind of change this prop is supposed to be additive about.
|
|
183
|
+
const locale = localeProp ?? "en-US";
|
|
161
184
|
const [query, setQuery] = React.useState("");
|
|
162
185
|
const [internalSort, setInternalSort] = React.useState(defaultSort ?? null);
|
|
163
186
|
// Column list the table actually renders. Seeded from `columns`; the header
|
|
@@ -281,13 +304,19 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
281
304
|
const bv = cellValue(col, b);
|
|
282
305
|
const cmp = typeof av === "number" && typeof bv === "number"
|
|
283
306
|
? av - bv
|
|
284
|
-
:
|
|
307
|
+
: // `localeProp`, NOT `locale`: the ?? "en-US" fallback belongs to
|
|
308
|
+
// formatting only. Passing it here would pin collation to en-US
|
|
309
|
+
// for every consumer on a non-en-US runtime and silently reorder
|
|
310
|
+
// their rows. No test can catch that substitution on a machine
|
|
311
|
+
// whose own default is en-US -- one was written, it could not
|
|
312
|
+
// fail, and it was removed -- so this comment is the guard.
|
|
313
|
+
asText(av).localeCompare(asText(bv), localeProp);
|
|
285
314
|
if (cmp !== 0)
|
|
286
315
|
return cmp * dir;
|
|
287
316
|
}
|
|
288
317
|
return 0;
|
|
289
318
|
});
|
|
290
|
-
}, [filtered, sorts, colByKey]);
|
|
319
|
+
}, [filtered, sorts, colByKey, localeProp]);
|
|
291
320
|
/**
|
|
292
321
|
* Rows bucketed under a heading, preserving first-seen group order (a
|
|
293
322
|
* grouped view without reordering within a group). One bucket with a null
|
|
@@ -364,7 +393,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
364
393
|
const row = rows.find((rw) => rw.id === selCell.id);
|
|
365
394
|
const col = columns.find((cc) => cc.key === selCell.key);
|
|
366
395
|
if (row && col)
|
|
367
|
-
navigator.clipboard?.writeText(formatCell(col.type, cellValue(col, row), col.symbol));
|
|
396
|
+
navigator.clipboard?.writeText(formatCell(col.type, cellValue(col, row), col.symbol, locale));
|
|
368
397
|
e.preventDefault();
|
|
369
398
|
return;
|
|
370
399
|
}
|
|
@@ -568,7 +597,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
568
597
|
}
|
|
569
598
|
}, children: col.render
|
|
570
599
|
? col.render(value, row)
|
|
571
|
-
: renderTyped(col, value, avatarTone, col.format?.(value, row)) }, col.key));
|
|
600
|
+
: renderTyped(col, value, avatarTone, col.format?.(value, row), locale) }, col.key));
|
|
572
601
|
})] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), columns.map((col, i) => ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...stickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
573
602
|
}
|
|
574
603
|
DataTable.displayName = "DataTable";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Date-only cell values, parsed and formatted so the calendar day survives the
|
|
4
|
+
* reader's timezone.
|
|
5
|
+
*
|
|
6
|
+
* `new Date("1998-09-04")` is **UTC midnight** — ECMA-262 gives the date-only
|
|
7
|
+
* ISO forms a UTC interpretation — so formatting it in the reader's timezone
|
|
8
|
+
* renders it a day early for everyone west of UTC. A cell value like that
|
|
9
|
+
* names a *day*, and a day does not move when the reader does.
|
|
10
|
+
*
|
|
11
|
+
* Shared by both tables on purpose. RecordsTable was fixed in #220 and
|
|
12
|
+
* DataTable was not, and it took #221 to notice — the fourth time a defect has
|
|
13
|
+
* been fixed in one of the pair and left in the other. One implementation is
|
|
14
|
+
* the only version of this that stays fixed.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.parseCellDate = parseCellDate;
|
|
18
|
+
exports.formatCellDate = formatCellDate;
|
|
19
|
+
/** `YYYY`, `YYYY-MM`, `YYYY-MM-DD` — the forms ECMA-262 reads as UTC. */
|
|
20
|
+
const DATE_ONLY = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/;
|
|
21
|
+
function parseCellDate(raw) {
|
|
22
|
+
if (!raw)
|
|
23
|
+
return null;
|
|
24
|
+
const m = DATE_ONLY.exec(raw);
|
|
25
|
+
// the local-midnight constructor, so the calendar day survives the render.
|
|
26
|
+
// A string that carries a time is left alone: it names an instant, and an
|
|
27
|
+
// instant genuinely does move with the reader.
|
|
28
|
+
const d = m
|
|
29
|
+
? new Date(Number(m[1]), Number(m[2] ?? "1") - 1, Number(m[3] ?? "1"))
|
|
30
|
+
: new Date(raw);
|
|
31
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
32
|
+
}
|
|
33
|
+
/** The inverse: the picked date's own calendar day, not the UTC one. */
|
|
34
|
+
function formatCellDate(d) {
|
|
35
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
36
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
37
|
+
}
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -14,7 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = void 0;
|
|
17
|
+
exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = void 0;
|
|
18
|
+
var cell_date_1 = require("./cell-date");
|
|
19
|
+
Object.defineProperty(exports, "formatCellDate", { enumerable: true, get: function () { return cell_date_1.formatCellDate; } });
|
|
20
|
+
Object.defineProperty(exports, "parseCellDate", { enumerable: true, get: function () { return cell_date_1.parseCellDate; } });
|
|
18
21
|
var cx_1 = require("./cx");
|
|
19
22
|
Object.defineProperty(exports, "cx", { enumerable: true, get: function () { return cx_1.cx; } });
|
|
20
23
|
var define_1 = require("./define");
|
|
@@ -33,10 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.defaultRows = exports.defaultColumns = exports.recordsTableLabels = void 0;
|
|
36
|
+
exports.defaultRows = exports.defaultColumns = exports.parseCellDate = exports.formatCellDate = exports.recordsTableLabels = void 0;
|
|
37
37
|
exports.tagTone = tagTone;
|
|
38
|
-
exports.parseCellDate = parseCellDate;
|
|
39
|
-
exports.formatCellDate = formatCellDate;
|
|
40
38
|
exports.RecordsTable = RecordsTable;
|
|
41
39
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
42
40
|
const lucide_react_1 = require("lucide-react");
|
|
@@ -336,26 +334,19 @@ function TagsCell({ column, row, options, onChange, labels, }) {
|
|
|
336
334
|
* A string that carries a time is left alone: that one really is an instant,
|
|
337
335
|
* and shifting it would move the moment it names.
|
|
338
336
|
*/
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
349
|
-
}
|
|
350
|
-
/** The inverse: the picked date's own calendar day, not the UTC one. */
|
|
351
|
-
function formatCellDate(d) {
|
|
352
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
353
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
354
|
-
}
|
|
337
|
+
// parseCellDate/formatCellDate moved to core/cell-date.ts so DataTable shares
|
|
338
|
+
// one implementation rather than a second copy -- #221 was this defect fixed
|
|
339
|
+
// here (#220) and left in DataTable. Re-exported because they were already
|
|
340
|
+
// reachable from this module's public surface.
|
|
341
|
+
// imported for use here AND re-exported, because a bare `export ... from` does
|
|
342
|
+
// not bind the names locally
|
|
343
|
+
const core_2 = require("../../core");
|
|
344
|
+
Object.defineProperty(exports, "formatCellDate", { enumerable: true, get: function () { return core_2.formatCellDate; } });
|
|
345
|
+
Object.defineProperty(exports, "parseCellDate", { enumerable: true, get: function () { return core_2.parseCellDate; } });
|
|
355
346
|
function DateCell({ column, row, onChange, }) {
|
|
356
347
|
const raw = row[column.key];
|
|
357
|
-
const value = parseCellDate(raw);
|
|
358
|
-
return ((0, jsx_runtime_1.jsx)(datepickerfield_1.DatePickerField, { variant: "ghost", "aria-label": column.label, value: value, onChange: (d) => onChange(formatCellDate(d)) }));
|
|
348
|
+
const value = (0, core_2.parseCellDate)(raw);
|
|
349
|
+
return ((0, jsx_runtime_1.jsx)(datepickerfield_1.DatePickerField, { variant: "ghost", "aria-label": column.label, value: value, onChange: (d) => onChange((0, core_2.formatCellDate)(d)) }));
|
|
359
350
|
}
|
|
360
351
|
/**
|
|
361
352
|
* The "New <object>" create modal — measured from a create-record dialog:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ArrowDown, ArrowUp, Check, ChevronsUpDown, GripVertical, Plus, Search, SlidersHorizontal, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { isRtlElement } from "../../core/index.js";
|
|
4
|
+
import { formatCellDate, isRtlElement, parseCellDate } from "../../core/index.js";
|
|
5
5
|
import { Checkbox } from "../checkbox/index.js";
|
|
6
6
|
import { Menu, MenuItem } from "../menu/index.js";
|
|
7
7
|
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "../table/index.js";
|
|
@@ -10,12 +10,15 @@ import { datatableRecipe } from "./datatable.recipe.js";
|
|
|
10
10
|
* a display string. Returns "" when it cannot be parsed, so the field opens
|
|
11
11
|
* empty rather than showing an invalid value. */
|
|
12
12
|
function toDateInputValue(v) {
|
|
13
|
+
// formatCellDate, not toISOString().slice(0, 10): the latter takes the UTC
|
|
14
|
+
// day of a local Date, so a value picked late in the evening east of UTC
|
|
15
|
+
// round-trips as the next day (#221).
|
|
13
16
|
if (v instanceof Date && !Number.isNaN(v.getTime()))
|
|
14
|
-
return v
|
|
17
|
+
return formatCellDate(v);
|
|
15
18
|
if (typeof v === "string" || typeof v === "number") {
|
|
16
|
-
const d =
|
|
17
|
-
if (
|
|
18
|
-
return d
|
|
19
|
+
const d = parseCellDate(String(v));
|
|
20
|
+
if (d)
|
|
21
|
+
return formatCellDate(d);
|
|
19
22
|
}
|
|
20
23
|
return "";
|
|
21
24
|
}
|
|
@@ -53,21 +56,34 @@ function initials(name) {
|
|
|
53
56
|
const p = name.trim().split(/\s+/);
|
|
54
57
|
return ((p[0]?.[0] ?? "") + (p[1]?.[0] ?? "")).toUpperCase() || "?";
|
|
55
58
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// One Intl.DateTimeFormat per locale, built lazily and reused — constructing
|
|
60
|
+
// a formatter per cell render would be wasteful on a large grid.
|
|
61
|
+
const DATE_FMT_CACHE = new Map();
|
|
62
|
+
function dateFmt(locale) {
|
|
63
|
+
let fmt = DATE_FMT_CACHE.get(locale);
|
|
64
|
+
if (!fmt) {
|
|
65
|
+
fmt = new Intl.DateTimeFormat(locale, {
|
|
66
|
+
month: "short",
|
|
67
|
+
day: "numeric",
|
|
68
|
+
year: "numeric",
|
|
69
|
+
});
|
|
70
|
+
DATE_FMT_CACHE.set(locale, fmt);
|
|
71
|
+
}
|
|
72
|
+
return fmt;
|
|
73
|
+
}
|
|
74
|
+
function formatCell(type, v, symbol, locale = "en-US") {
|
|
62
75
|
if (type === "number")
|
|
63
|
-
return typeof v === "number" ? v.toLocaleString(
|
|
76
|
+
return typeof v === "number" ? v.toLocaleString(locale) : asText(v);
|
|
64
77
|
if (type === "currency")
|
|
65
78
|
return typeof v === "number"
|
|
66
|
-
? `${symbol ?? "$"}${v.toLocaleString(
|
|
79
|
+
? `${symbol ?? "$"}${v.toLocaleString(locale)}`
|
|
67
80
|
: asText(v);
|
|
68
81
|
if (type === "date") {
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
// parseCellDate, not `new Date(v)`: a date-only value is UTC midnight per
|
|
83
|
+
// ECMA-262, and formatting that in the reader's timezone renders it a day
|
|
84
|
+
// early west of UTC (#221, the same defect #220 fixed in RecordsTable).
|
|
85
|
+
const d = v ? parseCellDate(String(v)) : null;
|
|
86
|
+
return d ? dateFmt(locale).format(d) : asText(v);
|
|
71
87
|
}
|
|
72
88
|
return asText(v);
|
|
73
89
|
}
|
|
@@ -79,7 +95,7 @@ function formatCell(type, v, symbol) {
|
|
|
79
95
|
// silently drop the cell's data. Each branch decides for itself where a
|
|
80
96
|
// display string belongs -- as an entity's name, a status' label -- while the
|
|
81
97
|
// raw value keeps driving structure and tone.
|
|
82
|
-
function renderTyped(col, value, avatarTone, display) {
|
|
98
|
+
function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
83
99
|
const empty = _jsx("span", { className: "crisp-datatable-muted", children: "\u2014" });
|
|
84
100
|
switch (col.type) {
|
|
85
101
|
case "entity": {
|
|
@@ -110,7 +126,7 @@ function renderTyped(col, value, avatarTone, display) {
|
|
|
110
126
|
return (_jsx("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
|
|
111
127
|
}
|
|
112
128
|
default: {
|
|
113
|
-
const text = display || formatCell(col.type, value, col.symbol);
|
|
129
|
+
const text = display || formatCell(col.type, value, col.symbol, locale);
|
|
114
130
|
const isNum = col.type === "number" || col.type === "currency";
|
|
115
131
|
if (!text)
|
|
116
132
|
return empty;
|
|
@@ -119,9 +135,16 @@ function renderTyped(col, value, avatarTone, display) {
|
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
const CHECK_W = 40;
|
|
122
|
-
export function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
|
|
138
|
+
export function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
|
|
123
139
|
? `${total} ${total === 1 ? "row" : "rows"}`
|
|
124
140
|
: `${shown} of ${total}`, toolbar, className, reorderRowLabel = "Reorder row", ...rest }) {
|
|
141
|
+
// Two different defaults on purpose. FORMATTING falls back to "en-US", which
|
|
142
|
+
// is what the built-in date/number/currency renderers have always produced,
|
|
143
|
+
// so no existing output moves. SORTING falls back to `undefined`, which is
|
|
144
|
+
// the runtime's own collation — passing "en-US" there would silently change
|
|
145
|
+
// the row order for every consumer on a non-en-US runtime, which is exactly
|
|
146
|
+
// the kind of change this prop is supposed to be additive about.
|
|
147
|
+
const locale = localeProp ?? "en-US";
|
|
125
148
|
const [query, setQuery] = React.useState("");
|
|
126
149
|
const [internalSort, setInternalSort] = React.useState(defaultSort ?? null);
|
|
127
150
|
// Column list the table actually renders. Seeded from `columns`; the header
|
|
@@ -245,13 +268,19 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
245
268
|
const bv = cellValue(col, b);
|
|
246
269
|
const cmp = typeof av === "number" && typeof bv === "number"
|
|
247
270
|
? av - bv
|
|
248
|
-
:
|
|
271
|
+
: // `localeProp`, NOT `locale`: the ?? "en-US" fallback belongs to
|
|
272
|
+
// formatting only. Passing it here would pin collation to en-US
|
|
273
|
+
// for every consumer on a non-en-US runtime and silently reorder
|
|
274
|
+
// their rows. No test can catch that substitution on a machine
|
|
275
|
+
// whose own default is en-US -- one was written, it could not
|
|
276
|
+
// fail, and it was removed -- so this comment is the guard.
|
|
277
|
+
asText(av).localeCompare(asText(bv), localeProp);
|
|
249
278
|
if (cmp !== 0)
|
|
250
279
|
return cmp * dir;
|
|
251
280
|
}
|
|
252
281
|
return 0;
|
|
253
282
|
});
|
|
254
|
-
}, [filtered, sorts, colByKey]);
|
|
283
|
+
}, [filtered, sorts, colByKey, localeProp]);
|
|
255
284
|
/**
|
|
256
285
|
* Rows bucketed under a heading, preserving first-seen group order (a
|
|
257
286
|
* grouped view without reordering within a group). One bucket with a null
|
|
@@ -328,7 +357,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
328
357
|
const row = rows.find((rw) => rw.id === selCell.id);
|
|
329
358
|
const col = columns.find((cc) => cc.key === selCell.key);
|
|
330
359
|
if (row && col)
|
|
331
|
-
navigator.clipboard?.writeText(formatCell(col.type, cellValue(col, row), col.symbol));
|
|
360
|
+
navigator.clipboard?.writeText(formatCell(col.type, cellValue(col, row), col.symbol, locale));
|
|
332
361
|
e.preventDefault();
|
|
333
362
|
return;
|
|
334
363
|
}
|
|
@@ -532,7 +561,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
532
561
|
}
|
|
533
562
|
}, children: col.render
|
|
534
563
|
? col.render(value, row)
|
|
535
|
-
: renderTyped(col, value, avatarTone, col.format?.(value, row)) }, col.key));
|
|
564
|
+
: renderTyped(col, value, avatarTone, col.format?.(value, row), locale) }, col.key));
|
|
536
565
|
})] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && (_jsxs(TableFooter, { children: [hasSummary && (_jsxs(TableRow, { children: [showGrip && _jsx(TableCell, { ...stickyCol(-2) }), selectable && _jsx(TableCell, { ...stickyCol(-1) }), columns.map((col, i) => (_jsx(TableCell, { align: alignFor(col), ...stickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && (_jsx(TableRow, { children: _jsx(TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: _jsxs("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
537
566
|
}
|
|
538
567
|
DataTable.displayName = "DataTable";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date-only cell values, parsed and formatted so the calendar day survives the
|
|
3
|
+
* reader's timezone.
|
|
4
|
+
*
|
|
5
|
+
* `new Date("1998-09-04")` is **UTC midnight** — ECMA-262 gives the date-only
|
|
6
|
+
* ISO forms a UTC interpretation — so formatting it in the reader's timezone
|
|
7
|
+
* renders it a day early for everyone west of UTC. A cell value like that
|
|
8
|
+
* names a *day*, and a day does not move when the reader does.
|
|
9
|
+
*
|
|
10
|
+
* Shared by both tables on purpose. RecordsTable was fixed in #220 and
|
|
11
|
+
* DataTable was not, and it took #221 to notice — the fourth time a defect has
|
|
12
|
+
* been fixed in one of the pair and left in the other. One implementation is
|
|
13
|
+
* the only version of this that stays fixed.
|
|
14
|
+
*/
|
|
15
|
+
/** `YYYY`, `YYYY-MM`, `YYYY-MM-DD` — the forms ECMA-262 reads as UTC. */
|
|
16
|
+
const DATE_ONLY = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/;
|
|
17
|
+
export function parseCellDate(raw) {
|
|
18
|
+
if (!raw)
|
|
19
|
+
return null;
|
|
20
|
+
const m = DATE_ONLY.exec(raw);
|
|
21
|
+
// the local-midnight constructor, so the calendar day survives the render.
|
|
22
|
+
// A string that carries a time is left alone: it names an instant, and an
|
|
23
|
+
// instant genuinely does move with the reader.
|
|
24
|
+
const d = m
|
|
25
|
+
? new Date(Number(m[1]), Number(m[2] ?? "1") - 1, Number(m[3] ?? "1"))
|
|
26
|
+
: new Date(raw);
|
|
27
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
28
|
+
}
|
|
29
|
+
/** The inverse: the picked date's own calendar day, not the UTC one. */
|
|
30
|
+
export function formatCellDate(d) {
|
|
31
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
32
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
33
|
+
}
|
package/dist/esm/core/index.js
CHANGED
|
@@ -296,22 +296,14 @@ function TagsCell({ column, row, options, onChange, labels, }) {
|
|
|
296
296
|
* A string that carries a time is left alone: that one really is an instant,
|
|
297
297
|
* and shifting it would move the moment it names.
|
|
298
298
|
*/
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
: new Date(raw);
|
|
308
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
309
|
-
}
|
|
310
|
-
/** The inverse: the picked date's own calendar day, not the UTC one. */
|
|
311
|
-
export function formatCellDate(d) {
|
|
312
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
313
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
314
|
-
}
|
|
299
|
+
// parseCellDate/formatCellDate moved to core/cell-date.ts so DataTable shares
|
|
300
|
+
// one implementation rather than a second copy -- #221 was this defect fixed
|
|
301
|
+
// here (#220) and left in DataTable. Re-exported because they were already
|
|
302
|
+
// reachable from this module's public surface.
|
|
303
|
+
// imported for use here AND re-exported, because a bare `export ... from` does
|
|
304
|
+
// not bind the names locally
|
|
305
|
+
import { formatCellDate, parseCellDate } from "../../core/index.js";
|
|
306
|
+
export { formatCellDate, parseCellDate };
|
|
315
307
|
function DateCell({ column, row, onChange, }) {
|
|
316
308
|
const raw = row[column.key];
|
|
317
309
|
const value = parseCellDate(raw);
|
|
@@ -129,6 +129,12 @@ export type DataTableProps<T extends {
|
|
|
129
129
|
onEdit?: (id: string, key: string, value: string) => void;
|
|
130
130
|
/** Sticky the first column(s) with a horizontal scroll shadow (default true). */
|
|
131
131
|
stickyFirst?: boolean;
|
|
132
|
+
/** BCP 47 locale for the built-in `date`/`number`/`currency` renderers
|
|
133
|
+
* (default "en-US"). Formats the date and groups the number the way that
|
|
134
|
+
* locale's readers expect — e.g. "mn-MN" renders a date as "2026.09.12"
|
|
135
|
+
* instead of "Sep 12, 2026". Currency's symbol still comes from the
|
|
136
|
+
* column's `symbol`; this only changes digit grouping/date order. */
|
|
137
|
+
locale?: string;
|
|
132
138
|
/** Tone for `entity`-type avatars: `"brand"` (uniform, matches the real
|
|
133
139
|
* records grid) or `"hashed"` (a stable colour per name — the tone `tags`/
|
|
134
140
|
* `status` pills already use, and entity's previous default). */
|
|
@@ -157,7 +163,7 @@ export type DataTableProps<T extends {
|
|
|
157
163
|
};
|
|
158
164
|
export declare function DataTable<T extends {
|
|
159
165
|
id: string;
|
|
160
|
-
}>({ columns: columnsProp, data, selectable, selectedIds, onSelectionChange, selectAllLabel, selectRowLabel, selectionAnnouncement, columnMenu, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable, searchPlaceholder, searchLabel, onEdit, stickyFirst, avatarTone, surface, interactive, onRowClick, onAddRow, addRowLabel, empty, showCount, countLabel, toolbar, className, reorderRowLabel, ...rest }: DataTableProps<T>): React.JSX.Element;
|
|
166
|
+
}>({ columns: columnsProp, data, selectable, selectedIds, onSelectionChange, selectAllLabel, selectRowLabel, selectionAnnouncement, columnMenu, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable, searchPlaceholder, searchLabel, onEdit, stickyFirst, locale: localeProp, avatarTone, surface, interactive, onRowClick, onAddRow, addRowLabel, empty, showCount, countLabel, toolbar, className, reorderRowLabel, ...rest }: DataTableProps<T>): React.JSX.Element;
|
|
161
167
|
export declare namespace DataTable {
|
|
162
168
|
var displayName: string;
|
|
163
169
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date-only cell values, parsed and formatted so the calendar day survives the
|
|
3
|
+
* reader's timezone.
|
|
4
|
+
*
|
|
5
|
+
* `new Date("1998-09-04")` is **UTC midnight** — ECMA-262 gives the date-only
|
|
6
|
+
* ISO forms a UTC interpretation — so formatting it in the reader's timezone
|
|
7
|
+
* renders it a day early for everyone west of UTC. A cell value like that
|
|
8
|
+
* names a *day*, and a day does not move when the reader does.
|
|
9
|
+
*
|
|
10
|
+
* Shared by both tables on purpose. RecordsTable was fixed in #220 and
|
|
11
|
+
* DataTable was not, and it took #221 to notice — the fourth time a defect has
|
|
12
|
+
* been fixed in one of the pair and left in the other. One implementation is
|
|
13
|
+
* the only version of this that stays fixed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseCellDate(raw: string | undefined): Date | null;
|
|
16
|
+
/** The inverse: the picked date's own calendar day, not the UTC one. */
|
|
17
|
+
export declare function formatCellDate(d: Date): string;
|
|
@@ -201,9 +201,19 @@ export type RecordsTableLabelsInput = Partial<Omit<RecordsTableLabels, "agg" | "
|
|
|
201
201
|
op?: Partial<Record<FilterOp, string>>;
|
|
202
202
|
};
|
|
203
203
|
type FilterOp = "contains" | "is" | "isnot" | "empty" | "notempty";
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
/**
|
|
205
|
+
* A date-only cell is a CALENDAR DATE, not an instant. ECMA-262 parses the
|
|
206
|
+
* date-only ISO forms — `1998`, `1998-09`, `1998-09-04` — as UTC midnight, and
|
|
207
|
+
* the field then renders that instant in the reader's own timezone. A
|
|
208
|
+
* `founded: "1998"` field therefore read **Dec 31, 1997** for every reader west
|
|
209
|
+
* of UTC, at every hour of every day, and a date picked east of UTC
|
|
210
|
+
* round-tripped back a day short through `toISOString()` (#220).
|
|
211
|
+
*
|
|
212
|
+
* A string that carries a time is left alone: that one really is an instant,
|
|
213
|
+
* and shifting it would move the moment it names.
|
|
214
|
+
*/
|
|
215
|
+
import { formatCellDate, parseCellDate } from "../../core";
|
|
216
|
+
export { formatCellDate, parseCellDate };
|
|
207
217
|
/**
|
|
208
218
|
* A measured-style record grid — fully featured. Toolbar (search / sort status /
|
|
209
219
|
* count / New), sticky checkbox + entity columns, 36px rows, typed cells,
|
|
@@ -215,4 +225,3 @@ export declare function formatCellDate(d: Date): string;
|
|
|
215
225
|
export declare function RecordsTable({ labels, columns: columnsProp, rows: rowsProp, selectedIds, onSelectionChange, showNew, allowAddColumn, onOpenRecord, onOpenRecordPage, avatarTone, className, ...rest }: RecordsTableProps): React.JSX.Element;
|
|
216
226
|
export declare const defaultColumns: Column[];
|
|
217
227
|
export declare const defaultRows: Row[];
|
|
218
|
-
export {};
|