@crisp-ui-kit/crisp 0.54.11 → 0.55.1
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/badge.css +1 -1
- package/dist/badge.layered.css +1 -1
- package/dist/cjs/components/badge/badge.recipe.js +10 -5
- package/dist/cjs/components/datatable/countries.js +73 -0
- package/dist/cjs/components/datatable/datatable.js +318 -40
- package/dist/cjs/components/datatable/datatable.recipe.js +18 -1
- package/dist/cjs/components/resizablepanel/resizablepanel.js +10 -1
- package/dist/cjs/components/table/table.js +8 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +110 -27
- package/dist/cjs/index.js +0 -2
- package/dist/cjs/product/recordpage/index.js +6 -1
- package/dist/cjs/product/recordpage/recordpage.js +20 -4
- package/dist/datatable.css +141 -102
- package/dist/datatable.layered.css +141 -102
- package/dist/esm/components/badge/badge.recipe.js +10 -5
- package/dist/esm/components/datatable/countries.js +68 -0
- package/dist/esm/components/datatable/datatable.js +319 -41
- package/dist/esm/components/datatable/datatable.recipe.js +18 -1
- package/dist/esm/components/resizablepanel/resizablepanel.js +10 -1
- package/dist/esm/components/table/table.js +8 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +111 -28
- package/dist/esm/index.js +0 -2
- package/dist/esm/product/recordpage/index.js +2 -0
- package/dist/esm/product/recordpage/recordpage.js +20 -4
- package/dist/styles.css +142 -375
- package/dist/styles.layered.css +142 -375
- package/dist/types/components/badge/badge.recipe.d.ts +3 -1
- package/dist/types/components/datatable/countries.d.ts +12 -0
- package/dist/types/components/datatable/datatable.d.ts +27 -3
- package/dist/types/components/table/table.d.ts +14 -0
- package/dist/types/index.d.ts +0 -2
- package/dist/types/product/recordpage/index.d.ts +2 -0
- package/package.json +2 -12
- package/dist/cjs/product/recordstable/company-logos.js +0 -29
- package/dist/cjs/product/recordstable/index.js +0 -13
- package/dist/cjs/product/recordstable/recordstable.js +0 -1362
- package/dist/cjs/product/recordstable/recordstable.recipe.js +0 -175
- package/dist/cjs/product/recordstable/recordstable.tag.js +0 -26
- package/dist/cjs/product/taskstable/index.js +0 -7
- package/dist/cjs/product/taskstable/taskstable.js +0 -32
- package/dist/cjs/product/taskstable/taskstable.recipe.js +0 -56
- package/dist/esm/product/recordstable/company-logos.js +0 -26
- package/dist/esm/product/recordstable/index.js +0 -3
- package/dist/esm/product/recordstable/recordstable.js +0 -1322
- package/dist/esm/product/recordstable/recordstable.recipe.js +0 -172
- package/dist/esm/product/recordstable/recordstable.tag.js +0 -23
- package/dist/esm/product/taskstable/index.js +0 -2
- package/dist/esm/product/taskstable/taskstable.js +0 -29
- package/dist/esm/product/taskstable/taskstable.recipe.js +0 -53
- package/dist/recordstable.css +0 -240
- package/dist/recordstable.d.ts +0 -7
- package/dist/recordstable.layered.css +0 -242
- package/dist/taskstable.css +0 -30
- package/dist/taskstable.d.ts +0 -7
- package/dist/taskstable.layered.css +0 -32
- package/dist/types/product/recordstable/company-logos.d.ts +0 -1
- package/dist/types/product/recordstable/index.d.ts +0 -3
- package/dist/types/product/recordstable/recordstable.d.ts +0 -294
- package/dist/types/product/recordstable/recordstable.recipe.d.ts +0 -5
- package/dist/types/product/recordstable/recordstable.tag.d.ts +0 -15
- package/dist/types/product/taskstable/index.d.ts +0 -2
- package/dist/types/product/taskstable/taskstable.d.ts +0 -59
- package/dist/types/product/taskstable/taskstable.recipe.d.ts +0 -3
|
@@ -52,13 +52,16 @@ const avatar_1 = require("../avatar");
|
|
|
52
52
|
const checkbox_1 = require("../checkbox");
|
|
53
53
|
const columncalc_1 = require("../columncalc");
|
|
54
54
|
const contextmenu_1 = require("../contextmenu");
|
|
55
|
+
const datepickerfield_1 = require("../datepickerfield");
|
|
55
56
|
const menu_1 = require("../menu");
|
|
56
57
|
const radiogroup_1 = require("../radiogroup");
|
|
57
58
|
const select_1 = require("../select");
|
|
58
59
|
const spinner_1 = require("../spinner");
|
|
59
60
|
const switch_1 = require("../switch");
|
|
60
61
|
const table_1 = require("../table");
|
|
62
|
+
const timepicker_1 = require("../timepicker");
|
|
61
63
|
const tooltip_1 = require("../tooltip");
|
|
64
|
+
const countries_1 = require("./countries");
|
|
62
65
|
const datatable_recipe_1 = require("./datatable.recipe");
|
|
63
66
|
// #534: whether an element's own content is being cut off by its own box --
|
|
64
67
|
// the same fact `.crisp-datatable-th-label`'s CSS ellipsis is silently
|
|
@@ -196,6 +199,17 @@ function asText(v) {
|
|
|
196
199
|
return v.join(", ");
|
|
197
200
|
return String(v);
|
|
198
201
|
}
|
|
202
|
+
// crisp/#765 -- a `type: "phone"`/`editor: "phone"` cell's raw value is
|
|
203
|
+
// "{iso2}|{digits}", not a bare E.164 string: see DataTableColumn's own
|
|
204
|
+
// "phone" doc comment for why the pipe separator earns its keep over a
|
|
205
|
+
// plain "+{dialCode}{digits}" concatenation.
|
|
206
|
+
function parsePhoneValue(v) {
|
|
207
|
+
const s = typeof v === "string" ? v : "";
|
|
208
|
+
const i = s.indexOf("|");
|
|
209
|
+
if (i < 0)
|
|
210
|
+
return null;
|
|
211
|
+
return { iso2: s.slice(0, i), digits: s.slice(i + 1) };
|
|
212
|
+
}
|
|
199
213
|
// stable pastel tone per string value — colour-coded tags/statuses
|
|
200
214
|
const TAG_TONES = [
|
|
201
215
|
"brand",
|
|
@@ -415,6 +429,13 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
415
429
|
return empty;
|
|
416
430
|
return (0, jsx_runtime_1.jsx)(RatingStars, { value: n, max: col.maxRating ?? 5 });
|
|
417
431
|
}
|
|
432
|
+
case "phone": {
|
|
433
|
+
const parsed = parsePhoneValue(value);
|
|
434
|
+
if (!parsed?.digits)
|
|
435
|
+
return empty;
|
|
436
|
+
const country = (0, countries_1.countryByIso2)(parsed.iso2);
|
|
437
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone", children: [country && (0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: (0, countries_1.flagEmoji)(country.iso2) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-dial", children: ["+", country?.dialCode ?? "?"] }), parsed.digits] }));
|
|
438
|
+
}
|
|
418
439
|
case "url":
|
|
419
440
|
case "email": {
|
|
420
441
|
const text = display || asText(value);
|
|
@@ -568,6 +589,92 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
|
|
|
568
589
|
onCommit(v);
|
|
569
590
|
} }) }));
|
|
570
591
|
}
|
|
592
|
+
// crisp/#765, confirmed live -- Attio's own Phone Number editor: a
|
|
593
|
+
// country-code select (ghost trigger, "MN ▾"), then a static, non-editable
|
|
594
|
+
// "+{dialCode}" prefix, then a borderless digits field -- measured
|
|
595
|
+
// `rgba(0,0,0,0.63)` for the prefix text, matching every other muted-text
|
|
596
|
+
// value already measured elsewhere in this file (14px in both cases).
|
|
597
|
+
// Reuses crisp's own searchable `Select` for the country list -- the same
|
|
598
|
+
// popover chrome every other picker in this file already uses -- rather
|
|
599
|
+
// than inventing new country-picker chrome; the flag/name/dial-code ROW
|
|
600
|
+
// shape wasn't independently re-measured against Attio's own popover this
|
|
601
|
+
// pass (time-boxed), so it's built to the same shape crisp's other search
|
|
602
|
+
// pickers already carry.
|
|
603
|
+
function DataPhoneEditor({ col, value, isPending, onCommit, }) {
|
|
604
|
+
const parsed = parsePhoneValue(value);
|
|
605
|
+
const [iso2, setIso2] = React.useState(parsed?.iso2 ?? "US");
|
|
606
|
+
const [digits, setDigits] = React.useState(parsed?.digits ?? "");
|
|
607
|
+
const country = (0, countries_1.countryByIso2)(iso2);
|
|
608
|
+
const countryOptions = countries_1.COUNTRIES.map((c) => ({
|
|
609
|
+
value: c.iso2,
|
|
610
|
+
label: `${(0, countries_1.flagEmoji)(c.iso2)} ${c.name} +${c.dialCode}`,
|
|
611
|
+
keywords: `${c.name} ${c.iso2} ${c.dialCode}`,
|
|
612
|
+
}));
|
|
613
|
+
const commit = (nextIso2, nextDigits) => onCommit(`${nextIso2}|${nextDigits}`);
|
|
614
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-editor", children: [(0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, "aria-label": `Edit ${col.key} country`, value: iso2, disabled: isPending, options: countryOptions, onValueChange: (v) => {
|
|
615
|
+
setIso2(v);
|
|
616
|
+
commit(v, digits);
|
|
617
|
+
} }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-dial", "aria-hidden": "true", children: ["+", country?.dialCode ?? "?"] }), (0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit", "aria-label": `Edit ${col.key} number`, value: digits, disabled: isPending, onChange: (e) => setDigits(e.currentTarget.value), onBlur: () => commit(iso2, digits), onKeyDown: (e) => {
|
|
618
|
+
if (e.key === "Enter")
|
|
619
|
+
commit(iso2, digits);
|
|
620
|
+
} })] }));
|
|
621
|
+
}
|
|
622
|
+
// crisp/#762, confirmed live -- Attio's own Timestamp editor is a calendar +
|
|
623
|
+
// time + preset picker, not the native `datetime-local` input `type:
|
|
624
|
+
// "datetime"` gets by default (that stays the DEFAULT, unchanged, for
|
|
625
|
+
// backward compatibility -- this is a new, opt-in `editor: "datetime"`).
|
|
626
|
+
// Composes crisp's own `DatePickerField` (calendar + presets, already the
|
|
627
|
+
// measured shape: Today/Tomorrow/Next week/No date chips under the
|
|
628
|
+
// calendar) with `TimePicker` (hour/minute wheel) side by side. The exact
|
|
629
|
+
// PRESET WORDING/order and the time control's own wheel-vs-text-field
|
|
630
|
+
// shape weren't independently re-verified against Attio's pixels this
|
|
631
|
+
// pass (time-boxed) -- `TimePicker` is crisp's only time-input primitive,
|
|
632
|
+
// reused rather than inventing a new one from scratch.
|
|
633
|
+
function DataDateTimeEditor({ col, value, isPending, onCommit, }) {
|
|
634
|
+
const initial = value instanceof Date
|
|
635
|
+
? value
|
|
636
|
+
: typeof value === "string" || typeof value === "number"
|
|
637
|
+
? new Date(value)
|
|
638
|
+
: null;
|
|
639
|
+
const hasInitial = initial != null && !Number.isNaN(initial.getTime());
|
|
640
|
+
const [date, setDate] = React.useState(hasInitial ? initial : null);
|
|
641
|
+
const [time, setTime] = React.useState({
|
|
642
|
+
hour: hasInitial ? initial.getHours() : 0,
|
|
643
|
+
minute: hasInitial ? initial.getMinutes() : 0,
|
|
644
|
+
});
|
|
645
|
+
const commit = (nextDate, nextTime) => {
|
|
646
|
+
if (!nextDate) {
|
|
647
|
+
onCommit("");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
651
|
+
onCommit(`${nextDate.getFullYear()}-${pad(nextDate.getMonth() + 1)}-${pad(nextDate.getDate())}T${pad(nextTime.hour)}:${pad(nextTime.minute)}`);
|
|
652
|
+
};
|
|
653
|
+
const preset = (label, days) => ({
|
|
654
|
+
label,
|
|
655
|
+
date: () => {
|
|
656
|
+
const d = new Date();
|
|
657
|
+
d.setDate(d.getDate() + days);
|
|
658
|
+
return d;
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-datetime-editor", children: [(0, jsx_runtime_1.jsx)(datepickerfield_1.DatePickerField, { "aria-label": `Edit ${col.key} date`, variant: "ghost",
|
|
662
|
+
// "No date" chip in Attio's own popover maps to DatePickerField's
|
|
663
|
+
// existing Clear affordance -- its own default label ("Clear
|
|
664
|
+
// date") is already overridable via `useCrispMessage` internally,
|
|
665
|
+
// so no hardcoded copy of this component's own is added here.
|
|
666
|
+
value: date, presets: [
|
|
667
|
+
preset("Today", 0),
|
|
668
|
+
preset("Tomorrow", 1),
|
|
669
|
+
preset("Next week", 7),
|
|
670
|
+
], onValueChange: (d) => {
|
|
671
|
+
setDate(d);
|
|
672
|
+
commit(d, time);
|
|
673
|
+
} }), (0, jsx_runtime_1.jsx)(timepicker_1.TimePicker, { size: "small", value: time, disabled: isPending, onValueChange: (t) => {
|
|
674
|
+
setTime(t);
|
|
675
|
+
commit(date, t);
|
|
676
|
+
} })] }));
|
|
677
|
+
}
|
|
571
678
|
function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel: selectAllLabelProp, selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement, columnMenu = false, onColumnsChange, columnOrder, onColumnOrderChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder: searchPlaceholderProp, searchLabel: searchLabelProp, onEdit, onViewEditHistory, stickyFirst = true, stickyLast = false, scrollProps, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel: addRowLabelProp, empty: emptyProp, showCount = true, countLabel, toolbar, height, className, reorderRowLabel: reorderRowLabelProp, openPreviewLabel: openPreviewLabelProp, sortAscendingLabel: sortAscendingLabelProp, sortDescendingLabel: sortDescendingLabelProp, moveColumnLeftLabel: moveColumnLeftLabelProp, moveColumnRightLabel: moveColumnRightLabelProp, editColumnLabel: editColumnLabelProp, hideColumnLabel: hideColumnLabelProp, renameColumnLabel: renameColumnLabelProp, viewSettingsLabel: viewSettingsLabelProp, columnsGroupLabel: columnsGroupLabelProp, cellMenuLabel: cellMenuLabelProp, copyLabel: copyLabelProp, pasteLabel: pasteLabelProp, viewEditHistoryLabel: viewEditHistoryLabelProp, clearValueLabel: clearValueLabelProp, ...rest }) {
|
|
572
679
|
// crisp/#471: same resolution order as TableToolbar's own — an explicit
|
|
573
680
|
// prop wins, then the provider's translation of the built-in English
|
|
@@ -1071,14 +1178,48 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1071
1178
|
const colAriaIndex = (i) => ariaColIndexBase + i + 1;
|
|
1072
1179
|
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id));
|
|
1073
1180
|
const someSelected = selected.size > 0 && !allSelected;
|
|
1181
|
+
// crisp/#782, ported from RecordsTable's own already-shipped logic: the
|
|
1182
|
+
// header checkbox speaks for the rows you can SEE. Rebuilding the whole
|
|
1183
|
+
// set from `rows` (the filtered/visible set) would silently drop any
|
|
1184
|
+
// selection an active filter is hiding -- add/remove only the visible
|
|
1185
|
+
// rows and leave the rest of `selected` alone.
|
|
1074
1186
|
function toggleAll() {
|
|
1075
|
-
|
|
1187
|
+
const next = new Set(selected);
|
|
1188
|
+
for (const r of rows) {
|
|
1189
|
+
if (allSelected)
|
|
1190
|
+
next.delete(r.id);
|
|
1191
|
+
else
|
|
1192
|
+
next.add(r.id);
|
|
1193
|
+
}
|
|
1194
|
+
setSelected(next);
|
|
1076
1195
|
}
|
|
1077
1196
|
function toggleRow(id) {
|
|
1078
1197
|
const next = new Set(selected);
|
|
1079
1198
|
next.has(id) ? next.delete(id) : next.add(id);
|
|
1080
1199
|
setSelected(next);
|
|
1081
1200
|
}
|
|
1201
|
+
// crisp/#781: Shift+Click on a row checkbox selects the whole contiguous
|
|
1202
|
+
// range from the last plain click (the anchor) to this row -- confirmed
|
|
1203
|
+
// live, the mouse counterpart to #749's Shift+Arrow keyboard gesture,
|
|
1204
|
+
// sharing the same `rowSelectAnchor` state and the same recompute-the-
|
|
1205
|
+
// whole-span shape (not accumulate) so shift-clicking back toward the
|
|
1206
|
+
// anchor shrinks the same way clicking away from it grows. A plain click
|
|
1207
|
+
// both toggles normally AND re-anchors, so the NEXT Shift+Click extends
|
|
1208
|
+
// from wherever the user last plain-clicked, matching Attio.
|
|
1209
|
+
function toggleRowOrExtend(id, shiftKey) {
|
|
1210
|
+
if (shiftKey && rowSelectAnchor != null) {
|
|
1211
|
+
const anchorIdx = rows.findIndex((r) => r.id === rowSelectAnchor);
|
|
1212
|
+
const idx = rows.findIndex((r) => r.id === id);
|
|
1213
|
+
if (anchorIdx >= 0 && idx >= 0) {
|
|
1214
|
+
const lo = Math.min(anchorIdx, idx);
|
|
1215
|
+
const hi = Math.max(anchorIdx, idx);
|
|
1216
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((r) => r.id)));
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
setRowSelectAnchor(id);
|
|
1221
|
+
toggleRow(id);
|
|
1222
|
+
}
|
|
1082
1223
|
function cycleSort(key) {
|
|
1083
1224
|
// Clicking a header drives a single sort, as before. Multi-sort is supplied
|
|
1084
1225
|
// by the caller through the `sort` prop (built in a sort popover).
|
|
@@ -1285,6 +1426,52 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1285
1426
|
setSelected(new Set(rows.map((rw) => rw.id)));
|
|
1286
1427
|
return;
|
|
1287
1428
|
}
|
|
1429
|
+
// crisp/#772: Cmd/Ctrl+ArrowUp jumps the selected cell to the first row,
|
|
1430
|
+
// Cmd/Ctrl+ArrowDown to the last -- confirmed live (Attio's own help
|
|
1431
|
+
// drawer documents "Jump to top"/"Jump to bottom" on these exact
|
|
1432
|
+
// bindings). `!e.shiftKey` guards this from stealing Cmd+Shift+Arrow,
|
|
1433
|
+
// the separate #773 gesture (extend-selection-to-first/last) handled
|
|
1434
|
+
// in its own branch right below.
|
|
1435
|
+
if ((e.metaKey || e.ctrlKey) &&
|
|
1436
|
+
!e.shiftKey &&
|
|
1437
|
+
(e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
1438
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1439
|
+
if (r0 < 0)
|
|
1440
|
+
return;
|
|
1441
|
+
e.preventDefault();
|
|
1442
|
+
const nr0 = e.key === "ArrowUp" ? 0 : rows.length - 1;
|
|
1443
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1444
|
+
clipboardProxyRef.current?.select();
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
// crisp/#773: Cmd/Ctrl+Shift+Up/Down extends the row-checkbox selection
|
|
1448
|
+
// all the way to the first/last row in one keystroke -- confirmed live,
|
|
1449
|
+
// the "jump" sibling of #749's one-step Shift+Arrow below, sharing the
|
|
1450
|
+
// same anchor→destination-span recompute (only the destination differs:
|
|
1451
|
+
// 0 / rows.length-1 instead of r±1). Checked ahead of #749's own branch
|
|
1452
|
+
// so the modifier routes here instead of being swallowed as a one-step
|
|
1453
|
+
// Shift+Arrow.
|
|
1454
|
+
if (selectable &&
|
|
1455
|
+
(e.metaKey || e.ctrlKey) &&
|
|
1456
|
+
e.shiftKey &&
|
|
1457
|
+
(e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
1458
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1459
|
+
if (r0 < 0)
|
|
1460
|
+
return;
|
|
1461
|
+
const nr0 = e.key === "ArrowDown" ? rows.length - 1 : 0;
|
|
1462
|
+
e.preventDefault();
|
|
1463
|
+
const anchorId = rowSelectAnchor ?? selCell.id;
|
|
1464
|
+
const anchorIdx = rows.findIndex((row) => row.id === anchorId);
|
|
1465
|
+
if (anchorIdx < 0)
|
|
1466
|
+
return;
|
|
1467
|
+
setRowSelectAnchor(anchorId);
|
|
1468
|
+
const lo = Math.min(anchorIdx, nr0);
|
|
1469
|
+
const hi = Math.max(anchorIdx, nr0);
|
|
1470
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((row) => row.id)));
|
|
1471
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1472
|
+
clipboardProxyRef.current?.select();
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1288
1475
|
// crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
|
|
1289
1476
|
// confirmed live, distinct from Shift+Click's cell-range gesture above
|
|
1290
1477
|
// (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
|
|
@@ -1315,12 +1502,38 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1315
1502
|
clipboardProxyRef.current?.select();
|
|
1316
1503
|
return;
|
|
1317
1504
|
}
|
|
1505
|
+
// crisp/#770: Shift+Space toggles the current row's own checkbox without
|
|
1506
|
+
// moving the selected cell or touching any other row -- distinct from
|
|
1507
|
+
// #749/#773 above, whose Shift+Arrow gestures move the cursor as they
|
|
1508
|
+
// extend the range.
|
|
1509
|
+
if (selectable && e.shiftKey && e.key === " ") {
|
|
1510
|
+
e.preventDefault();
|
|
1511
|
+
toggleRow(selCell.id);
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
// crisp/#771: Cmd/Ctrl+Enter always opens the row's record page,
|
|
1515
|
+
// regardless of whether the current column is editable -- distinct from
|
|
1516
|
+
// plain Enter below, which opens the cell's own editor first when one
|
|
1517
|
+
// exists.
|
|
1518
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
1519
|
+
if (onRowClick) {
|
|
1520
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1521
|
+
if (r0 < 0)
|
|
1522
|
+
return;
|
|
1523
|
+
e.preventDefault();
|
|
1524
|
+
onRowClick(rows[r0]);
|
|
1525
|
+
}
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1318
1528
|
const r = rows.findIndex((row) => row.id === selCell.id);
|
|
1319
1529
|
const c = columns.findIndex((col) => col.key === selCell.key);
|
|
1320
1530
|
if (r < 0 || c < 0)
|
|
1321
1531
|
return;
|
|
1322
1532
|
let nr = r;
|
|
1323
1533
|
let nc = c;
|
|
1534
|
+
// #772/#773 (Cmd/Ctrl±Shift+Up/Down) are both handled in their own
|
|
1535
|
+
// early-return branches above, before `r`/`c` are even computed -- a
|
|
1536
|
+
// plain arrow (no modifier) is the only thing that reaches here.
|
|
1324
1537
|
if (e.key === "ArrowUp")
|
|
1325
1538
|
nr = Math.max(0, r - 1);
|
|
1326
1539
|
else if (e.key === "ArrowDown")
|
|
@@ -1332,6 +1545,33 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1332
1545
|
const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
1333
1546
|
nc = forward ? Math.min(columns.length - 1, c + 1) : Math.max(0, c - 1);
|
|
1334
1547
|
}
|
|
1548
|
+
// crisp/#769: Tab/Shift+Tab move to the next/previous cell in reading
|
|
1549
|
+
// order, wrapping to the next (or previous) row at a row boundary. This
|
|
1550
|
+
// is pure logical/DOM order, not a visual direction, so unlike Left/Right
|
|
1551
|
+
// above it needs no RTL handling -- native tab order doesn't flip under
|
|
1552
|
+
// RTL either. At the grid's first/last cell there's nowhere left to go:
|
|
1553
|
+
// return without `preventDefault`, so Tab still falls through to the
|
|
1554
|
+
// browser's native focus order instead of getting trapped.
|
|
1555
|
+
else if (e.key === "Tab" && !e.shiftKey) {
|
|
1556
|
+
if (c + 1 < columns.length)
|
|
1557
|
+
nc = c + 1;
|
|
1558
|
+
else if (r + 1 < rows.length) {
|
|
1559
|
+
nr = r + 1;
|
|
1560
|
+
nc = 0;
|
|
1561
|
+
}
|
|
1562
|
+
else
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
else if (e.key === "Tab" && e.shiftKey) {
|
|
1566
|
+
if (c - 1 >= 0)
|
|
1567
|
+
nc = c - 1;
|
|
1568
|
+
else if (r - 1 >= 0) {
|
|
1569
|
+
nr = r - 1;
|
|
1570
|
+
nc = columns.length - 1;
|
|
1571
|
+
}
|
|
1572
|
+
else
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1335
1575
|
// crisp/#607, G-066/G-068: End jumps to the last row (confirmed live --
|
|
1336
1576
|
// it also triggers Attio's own lazy-load, which is a data-loading
|
|
1337
1577
|
// concern outside this component; the #577 scroll-into-view effect
|
|
@@ -1436,15 +1676,21 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1436
1676
|
}
|
|
1437
1677
|
setEditing(null);
|
|
1438
1678
|
}
|
|
1679
|
+
const reduceMotion = (0, core_1.usePrefersReducedMotion)();
|
|
1439
1680
|
// crisp/#577 -- keyboard nav to an off-screen (virtualized-out) cell
|
|
1440
|
-
// scrolls it into the rendered window, on both axes.
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1681
|
+
// scrolls it into the rendered window, on both axes. `renderItems`/
|
|
1682
|
+
// `rowOffsets` are computed unconditionally (`height == null` just makes
|
|
1683
|
+
// them a no-op elsewhere, see the comment above `renderItems`), so this
|
|
1684
|
+
// runs on both axes regardless of `height` too (crisp/#780 -- it used to
|
|
1685
|
+
// gate the vertical half on `height != null`, on the reasoning that only
|
|
1686
|
+
// `height` gives the grid an internal scroll position to move; that's
|
|
1687
|
+
// wrong when the grid's own container still scrolls without an explicit
|
|
1688
|
+
// height, e.g. inside a shorter ancestor, matching `RecordsTable`'s own
|
|
1689
|
+
// already-shipped #257 fix, which had no such gate). `widths` is read only
|
|
1690
|
+
// indirectly, through `colWidth`'s closure over it -- biome can't see
|
|
1691
|
+
// that, but a column resize while a cell is selected genuinely needs to
|
|
1692
|
+
// re-run this to keep it in view, so the dependency is real even though
|
|
1693
|
+
// the lint rule can't prove it.
|
|
1448
1694
|
// biome-ignore lint/correctness/useExhaustiveDependencies: widths is read via colWidth's closure, see comment above
|
|
1449
1695
|
React.useEffect(() => {
|
|
1450
1696
|
if (!selCell)
|
|
@@ -1452,28 +1698,37 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1452
1698
|
const el = scrollRef.current;
|
|
1453
1699
|
if (!el)
|
|
1454
1700
|
return;
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1457
|
-
//
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1701
|
+
// `el.scrollTo` rather than a direct `el.scrollTop =`/`el.scrollLeft =`
|
|
1702
|
+
// assignment (crisp/#780, matching RecordsTable's own #208/#257 rule):
|
|
1703
|
+
// property assignment is ALWAYS instant, so it can never honour a
|
|
1704
|
+
// consumer's own `scroll-behavior: smooth` on this container; `scrollTo`
|
|
1705
|
+
// defers to that CSS when no `behavior` is given, and only forces
|
|
1706
|
+
// `"auto"` under `prefers-reduced-motion`, which is the one case that
|
|
1707
|
+
// must win even over a host page's own smooth-scroll opt-in.
|
|
1708
|
+
const scrollTo = (opts) => el.scrollTo({
|
|
1709
|
+
...opts,
|
|
1710
|
+
...(reduceMotion ? { behavior: "auto" } : null),
|
|
1711
|
+
});
|
|
1712
|
+
// Sets `viewport` directly from the SAME value just passed to
|
|
1713
|
+
// `scrollTo`, rather than waiting for a follow-up `scroll` event to
|
|
1714
|
+
// round-trip through the `onScroll` handler above -- a real browser
|
|
1715
|
+
// does fire one when the scroll position changes programmatically, but
|
|
1716
|
+
// relying on that would make this effect's own visible result (the
|
|
1460
1717
|
// newly-selected cell actually rendering) depend on a second render
|
|
1461
1718
|
// triggered from outside React's own update, which is both slower and
|
|
1462
1719
|
// one more thing that has to be true for this to work.
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
setViewport((v) => ({ ...v, top: newTop }));
|
|
1476
|
-
}
|
|
1720
|
+
const itemIndex = rowIdToItemIndex.get(selCell.id);
|
|
1721
|
+
if (itemIndex != null) {
|
|
1722
|
+
const itemTop = rowOffsets[itemIndex];
|
|
1723
|
+
const itemBottom = itemTop + renderItems[itemIndex].height;
|
|
1724
|
+
if (itemTop < el.scrollTop) {
|
|
1725
|
+
scrollTo({ top: itemTop });
|
|
1726
|
+
setViewport((v) => ({ ...v, top: itemTop }));
|
|
1727
|
+
}
|
|
1728
|
+
else if (itemBottom > el.scrollTop + el.clientHeight) {
|
|
1729
|
+
const newTop = itemBottom - el.clientHeight;
|
|
1730
|
+
scrollTo({ top: newTop });
|
|
1731
|
+
setViewport((v) => ({ ...v, top: newTop }));
|
|
1477
1732
|
}
|
|
1478
1733
|
}
|
|
1479
1734
|
const ci = columns.findIndex((c) => c.key === selCell.key);
|
|
@@ -1483,24 +1738,24 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1483
1738
|
left += colWidth(columns[i]);
|
|
1484
1739
|
const right = left + colWidth(columns[ci]);
|
|
1485
1740
|
if (left < el.scrollLeft) {
|
|
1486
|
-
|
|
1741
|
+
scrollTo({ left });
|
|
1487
1742
|
setViewport((v) => ({ ...v, left }));
|
|
1488
1743
|
}
|
|
1489
1744
|
else if (right > el.scrollLeft + el.clientWidth) {
|
|
1490
1745
|
const newLeft = right - el.clientWidth;
|
|
1491
|
-
|
|
1746
|
+
scrollTo({ left: newLeft });
|
|
1492
1747
|
setViewport((v) => ({ ...v, left: newLeft }));
|
|
1493
1748
|
}
|
|
1494
1749
|
}
|
|
1495
1750
|
}, [
|
|
1496
1751
|
selCell,
|
|
1497
|
-
height,
|
|
1498
1752
|
columns,
|
|
1499
1753
|
widths,
|
|
1500
1754
|
selectable,
|
|
1501
1755
|
rowIdToItemIndex,
|
|
1502
1756
|
rowOffsets,
|
|
1503
1757
|
renderItems,
|
|
1758
|
+
reduceMotion,
|
|
1504
1759
|
]);
|
|
1505
1760
|
const totalCols = columns.length + (selectable ? 1 : 0) + (showGrip ? 1 : 0);
|
|
1506
1761
|
const hasSummary = columns.some((c) => c.summary || c.calc);
|
|
@@ -1874,12 +2129,33 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1874
2129
|
}, onDragEnd: () => {
|
|
1875
2130
|
setDragRowId(null);
|
|
1876
2131
|
setDropRowId(null);
|
|
1877
|
-
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
2132
|
+
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
2133
|
+
// preventDefault stops the native check/change
|
|
2134
|
+
// this click would otherwise also fire -- this
|
|
2135
|
+
// handler is the single source of truth so a
|
|
2136
|
+
// Shift+Click's range-select and a plain
|
|
2137
|
+
// click's toggle-and-re-anchor never race each
|
|
2138
|
+
// other (crisp/#781).
|
|
2139
|
+
onClick: (e) => {
|
|
2140
|
+
e.preventDefault();
|
|
2141
|
+
toggleRowOrExtend(row.id, e.shiftKey);
|
|
2142
|
+
}, onChange: () => { } }) }) })), colSlots.map((slot) => {
|
|
1878
2143
|
if (slot.type === "spacer") {
|
|
1879
2144
|
return ((0, jsx_runtime_1.jsx)("td", { role: "presentation", "aria-hidden": "true", style: { padding: 0, border: "none" } }, `__spacer_${slot.side}`));
|
|
1880
2145
|
}
|
|
1881
2146
|
const col = columns[slot.i];
|
|
1882
2147
|
const i = slot.i;
|
|
2148
|
+
// crisp/#779, matching RecordsTable's own #231 fix:
|
|
2149
|
+
// column 0 is the row's own identity (its name), not
|
|
2150
|
+
// a value it holds -- a screen reader crossing the
|
|
2151
|
+
// row should read "Google, Employees, 182,000", not a
|
|
2152
|
+
// bare "182,000". Spread onto every TableCell this
|
|
2153
|
+
// column can render as (every editor branch, not just
|
|
2154
|
+
// the default one), since the row-identity semantics
|
|
2155
|
+
// don't depend on which editor is open.
|
|
2156
|
+
const rowHeaderProps = i === 0
|
|
2157
|
+
? { as: "th", scope: "row" }
|
|
2158
|
+
: undefined;
|
|
1883
2159
|
const isEditing = editing?.id === row.id && editing?.key === col.key;
|
|
1884
2160
|
const value = cellValue(col, row);
|
|
1885
2161
|
const sticky = dataStickyCol(i);
|
|
@@ -1902,14 +2178,14 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1902
2178
|
// right). See the `isEditing` block below for their
|
|
1903
2179
|
// actual editor.
|
|
1904
2180
|
if (col.editable && col.editor === "switch") {
|
|
1905
|
-
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2181
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1906
2182
|
}
|
|
1907
2183
|
// crisp/#760: `editor: "rating"` is the same always-
|
|
1908
2184
|
// live shape as `switch` above -- clicking a star IS
|
|
1909
2185
|
// the commit gesture, confirmed live.
|
|
1910
2186
|
if (col.editable && col.editor === "rating") {
|
|
1911
2187
|
const n = typeof value === "number" ? value : Number(value);
|
|
1912
|
-
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(RatingStars, { "aria-label": `Edit ${col.key}`, value: Number.isNaN(n) ? 0 : n, max: col.maxRating ?? 5, onChange: (next) => commitEdit(row.id, col.key, String(next)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2188
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(RatingStars, { "aria-label": `Edit ${col.key}`, value: Number.isNaN(n) ? 0 : n, max: col.maxRating ?? 5, onChange: (next) => commitEdit(row.id, col.key, String(next)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1913
2189
|
}
|
|
1914
2190
|
// crisp/#761: `editor: "checkbox"` is a THIRD always-
|
|
1915
2191
|
// live boolean shape, distinct from `switch` --
|
|
@@ -1918,13 +2194,15 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1918
2194
|
// toggle. Reuses crisp's own `Checkbox` (already the
|
|
1919
2195
|
// measured 16px/r6/brand-fill grid checkbox).
|
|
1920
2196
|
if (col.editable && col.editor === "checkbox") {
|
|
1921
|
-
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onChange: (e) => commitEdit(row.id, col.key, String(e.currentTarget.checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2197
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onChange: (e) => commitEdit(row.id, col.key, String(e.currentTarget.checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1922
2198
|
}
|
|
1923
2199
|
if (isEditing &&
|
|
1924
2200
|
(col.editor === "select" ||
|
|
1925
2201
|
col.editor === "reference" ||
|
|
1926
|
-
col.editor === "actor"
|
|
1927
|
-
|
|
2202
|
+
col.editor === "actor" ||
|
|
2203
|
+
col.editor === "phone" ||
|
|
2204
|
+
col.editor === "datetime")) {
|
|
2205
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
|
|
1928
2206
|
// Same as the plain text editor's own cell:
|
|
1929
2207
|
// opening/using the picker must not also fire
|
|
1930
2208
|
// the row's onRowClick. Escape is handled by
|
|
@@ -1935,10 +2213,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1935
2213
|
// portaled popover, so a handler here can't
|
|
1936
2214
|
// rely on the key event ever bubbling through
|
|
1937
2215
|
// this element (#453).
|
|
1938
|
-
onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : ((0, jsx_runtime_1.jsx)(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2216
|
+
onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : col.editor === "phone" ? ((0, jsx_runtime_1.jsx)(DataPhoneEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : col.editor === "datetime" ? ((0, jsx_runtime_1.jsx)(DataDateTimeEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : ((0, jsx_runtime_1.jsx)(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1939
2217
|
}
|
|
1940
2218
|
if (isEditing) {
|
|
1941
|
-
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
|
|
2219
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
|
|
1942
2220
|
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
1943
2221
|
autoFocus: true, ...editorFor(col), "data-editor": editorFor(col).type, disabled: isPending, defaultValue: col.type === "date"
|
|
1944
2222
|
? toDateInputValue(value)
|
|
@@ -1997,7 +2275,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1997
2275
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1998
2276
|
expandedCell?.key === col.key;
|
|
1999
2277
|
const cellContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isExpanded && rendered, errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }));
|
|
2000
|
-
return ((0, jsx_runtime_1.jsx)(contextmenu_1.ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
|
|
2278
|
+
return ((0, jsx_runtime_1.jsx)(contextmenu_1.ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: col.editable
|
|
2001
2279
|
? "crisp-datatable-editable"
|
|
2002
2280
|
: undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
|
|
2003
2281
|
? ""
|
|
@@ -10,7 +10,15 @@ const tokens_1 = require("../../tokens");
|
|
|
10
10
|
const config = (0, core_1.defineRecipe)({
|
|
11
11
|
name: "datatable",
|
|
12
12
|
base: `display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2.5"]}; font-family: ${tokens_1.contract.font.sans};
|
|
13
|
-
|
|
13
|
+
/* The toolbar carried no horizontal inset of its own, so its content
|
|
14
|
+
(search box, sort/filter chips) sat flush at the table's outer edge
|
|
15
|
+
(x=0) while the header row's own th/td padding (table.recipe.ts,
|
|
16
|
+
#465) starts every column's text at 12px -- one row down, the SAME
|
|
17
|
+
grid's own header cell already begins at that inset. c.space["3"]
|
|
18
|
+
here just matches the row directly below it, measured live against
|
|
19
|
+
Attio (the sort/filter row's search chip lines up with the first
|
|
20
|
+
column header's text, not the grid's raw edge). */
|
|
21
|
+
& .crisp-datatable-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; min-height: 49px; border-top: 1px solid ${tokens_1.contract.borderSubtle}; padding: 0 ${tokens_1.contract.space["3"]}; }
|
|
14
22
|
& .crisp-datatable-search { position: relative; flex: 1; max-width: 280px; display: inline-flex; align-items: center; }
|
|
15
23
|
& .crisp-datatable-search svg { position: absolute; inset-inline-start: ${tokens_1.contract.space["2"]}; color: ${tokens_1.contract.fgMuted}; pointer-events: none; }
|
|
16
24
|
& .crisp-datatable-search input { width: 100%; height: 32px; padding: 0 ${tokens_1.contract.space["2"]} 0 ${tokens_1.contract.space["6"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; outline: none; transition: box-shadow .15s; }
|
|
@@ -275,6 +283,15 @@ const config = (0, core_1.defineRecipe)({
|
|
|
275
283
|
link. */
|
|
276
284
|
& .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
277
285
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
286
|
+
/* phone cell type/editor (#765) -- measured live: the dial-code prefix
|
|
287
|
+
is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
|
|
288
|
+
value already measured elsewhere in this file. */
|
|
289
|
+
& .crisp-datatable-phone { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; }
|
|
290
|
+
& .crisp-datatable-phone-dial { color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
|
|
291
|
+
& .crisp-datatable-phone-editor { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; }
|
|
292
|
+
& .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
|
|
293
|
+
/* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
|
|
294
|
+
& .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; }
|
|
278
295
|
/* rating cell type/editor (#760) -- measured live: 16px stars on an
|
|
279
296
|
18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
|
|
280
297
|
committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
|
|
@@ -163,6 +163,15 @@ function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeChange,
|
|
|
163
163
|
const finalWidth = rect ? Math.round(rect.width) : width;
|
|
164
164
|
commitSize(finalWidth);
|
|
165
165
|
}
|
|
166
|
+
// crisp/#795: double-click resets to `defaultSize`, confirmed live on
|
|
167
|
+
// Attio's own record-page divider (dragged 722px -> 924px, double-click
|
|
168
|
+
// snapped it back to exactly 722px -- its own default/initial width, not
|
|
169
|
+
// some other fixed value). Reuses `commitSize`, the same commit path a
|
|
170
|
+
// drag already uses, so persistence (`storageKey`) picks up the reset the
|
|
171
|
+
// same way it picks up a drag -- no separate reset-specific write.
|
|
172
|
+
function onHandleDoubleClick() {
|
|
173
|
+
commitSize(defaultSize);
|
|
174
|
+
}
|
|
166
175
|
const maxStyle = max == null ? undefined : typeof max === "number" ? `${max}px` : max;
|
|
167
176
|
const panelSizeStyle = {
|
|
168
177
|
width,
|
|
@@ -171,6 +180,6 @@ function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeChange,
|
|
|
171
180
|
};
|
|
172
181
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: (0, resizablepanel_recipe_1.resizablepanelRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-resizablepanel-spacer", style: floating ? { width: 0 } : { width, minWidth: min, maxWidth: maxStyle } }), (0, jsx_runtime_1.jsxs)("div", { ref: panelRef, className: "crisp-resizablepanel-panel", "data-side": side, "data-floating": floating ? "true" : "false", style: panelSizeStyle, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-resizablepanel-inner", children: children }), (0, jsx_runtime_1.jsx)("div", { className: handleVariant === "column"
|
|
173
182
|
? "crisp-resizablepanel-handle crisp-resizablepanel-handle--column"
|
|
174
|
-
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, children: handleVariant === "column" && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
183
|
+
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onDoubleClick: onHandleDoubleClick, children: handleVariant === "column" && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
175
184
|
}
|
|
176
185
|
ResizablePanel.displayName = "ResizablePanel";
|
|
@@ -64,7 +64,14 @@ exports.TableRow = React.forwardRef((props, ref) => (0, jsx_runtime_1.jsx)("tr",
|
|
|
64
64
|
exports.TableRow.displayName = "TableRow";
|
|
65
65
|
exports.TableHead = React.forwardRef(({ align, ...props }, ref) => (0, jsx_runtime_1.jsx)("th", { ref: ref, "data-align": align, ...props }));
|
|
66
66
|
exports.TableHead.displayName = "TableHead";
|
|
67
|
-
exports.TableCell = React.forwardRef(({ align, ...props }, ref) =>
|
|
67
|
+
exports.TableCell = React.forwardRef(({ align, as, ...props }, ref) => {
|
|
68
|
+
// Cast to "td", same as RecordsTable's own dynamic-tag precedent --
|
|
69
|
+
// `props` (which may carry `scope`) would otherwise need to satisfy
|
|
70
|
+
// both <td> and <th>'s JSX attribute types at once for a bare union
|
|
71
|
+
// element name.
|
|
72
|
+
const Cell = (as ?? "td");
|
|
73
|
+
return (0, jsx_runtime_1.jsx)(Cell, { ref: ref, "data-align": align, ...props });
|
|
74
|
+
});
|
|
68
75
|
exports.TableCell.displayName = "TableCell";
|
|
69
76
|
exports.TableCaption = React.forwardRef((props, ref) => (0, jsx_runtime_1.jsx)("caption", { ref: ref, ...props }));
|
|
70
77
|
exports.TableCaption.displayName = "TableCaption";
|