@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
|
@@ -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, CircleUserRound, ClipboardPaste, Copy, GripVertical, History, Lock, PanelRight, Plus, Search, SlidersHorizontal, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, } from "../../core/index.js";
|
|
4
|
+
import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, usePrefersReducedMotion, } from "../../core/index.js";
|
|
5
5
|
// crisp/#601 supersession: a deliberate exception to "DS atoms stay
|
|
6
6
|
// generic; product components compose them" (AUTHORING.md) -- the row's
|
|
7
7
|
// own comment affordance is `CommentCountBadge` (product/comments, #625),
|
|
@@ -14,13 +14,16 @@ import { Avatar } from "../avatar/index.js";
|
|
|
14
14
|
import { Checkbox } from "../checkbox/index.js";
|
|
15
15
|
import { ColumnCalc } from "../columncalc/index.js";
|
|
16
16
|
import { ContextMenu } from "../contextmenu/index.js";
|
|
17
|
+
import { DatePickerField } from "../datepickerfield/index.js";
|
|
17
18
|
import { Menu, MenuItem } from "../menu/index.js";
|
|
18
19
|
import { useRadioGroupKeyboard } from "../radiogroup/index.js";
|
|
19
20
|
import { Select } from "../select/index.js";
|
|
20
21
|
import { Spinner } from "../spinner/index.js";
|
|
21
22
|
import { Switch } from "../switch/index.js";
|
|
22
23
|
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "../table/index.js";
|
|
24
|
+
import { TimePicker } from "../timepicker/index.js";
|
|
23
25
|
import { Tooltip } from "../tooltip/index.js";
|
|
26
|
+
import { COUNTRIES, countryByIso2, flagEmoji } from "./countries.js";
|
|
24
27
|
import { datatableRecipe } from "./datatable.recipe.js";
|
|
25
28
|
// #534: whether an element's own content is being cut off by its own box --
|
|
26
29
|
// the same fact `.crisp-datatable-th-label`'s CSS ellipsis is silently
|
|
@@ -158,6 +161,17 @@ function asText(v) {
|
|
|
158
161
|
return v.join(", ");
|
|
159
162
|
return String(v);
|
|
160
163
|
}
|
|
164
|
+
// crisp/#765 -- a `type: "phone"`/`editor: "phone"` cell's raw value is
|
|
165
|
+
// "{iso2}|{digits}", not a bare E.164 string: see DataTableColumn's own
|
|
166
|
+
// "phone" doc comment for why the pipe separator earns its keep over a
|
|
167
|
+
// plain "+{dialCode}{digits}" concatenation.
|
|
168
|
+
function parsePhoneValue(v) {
|
|
169
|
+
const s = typeof v === "string" ? v : "";
|
|
170
|
+
const i = s.indexOf("|");
|
|
171
|
+
if (i < 0)
|
|
172
|
+
return null;
|
|
173
|
+
return { iso2: s.slice(0, i), digits: s.slice(i + 1) };
|
|
174
|
+
}
|
|
161
175
|
// stable pastel tone per string value — colour-coded tags/statuses
|
|
162
176
|
const TAG_TONES = [
|
|
163
177
|
"brand",
|
|
@@ -377,6 +391,13 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
377
391
|
return empty;
|
|
378
392
|
return _jsx(RatingStars, { value: n, max: col.maxRating ?? 5 });
|
|
379
393
|
}
|
|
394
|
+
case "phone": {
|
|
395
|
+
const parsed = parsePhoneValue(value);
|
|
396
|
+
if (!parsed?.digits)
|
|
397
|
+
return empty;
|
|
398
|
+
const country = countryByIso2(parsed.iso2);
|
|
399
|
+
return (_jsxs("span", { className: "crisp-datatable-phone", children: [country && _jsx("span", { "aria-hidden": "true", children: flagEmoji(country.iso2) }), _jsxs("span", { className: "crisp-datatable-phone-dial", children: ["+", country?.dialCode ?? "?"] }), parsed.digits] }));
|
|
400
|
+
}
|
|
380
401
|
case "url":
|
|
381
402
|
case "email": {
|
|
382
403
|
const text = display || asText(value);
|
|
@@ -530,6 +551,92 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
|
|
|
530
551
|
onCommit(v);
|
|
531
552
|
} }) }));
|
|
532
553
|
}
|
|
554
|
+
// crisp/#765, confirmed live -- Attio's own Phone Number editor: a
|
|
555
|
+
// country-code select (ghost trigger, "MN ▾"), then a static, non-editable
|
|
556
|
+
// "+{dialCode}" prefix, then a borderless digits field -- measured
|
|
557
|
+
// `rgba(0,0,0,0.63)` for the prefix text, matching every other muted-text
|
|
558
|
+
// value already measured elsewhere in this file (14px in both cases).
|
|
559
|
+
// Reuses crisp's own searchable `Select` for the country list -- the same
|
|
560
|
+
// popover chrome every other picker in this file already uses -- rather
|
|
561
|
+
// than inventing new country-picker chrome; the flag/name/dial-code ROW
|
|
562
|
+
// shape wasn't independently re-measured against Attio's own popover this
|
|
563
|
+
// pass (time-boxed), so it's built to the same shape crisp's other search
|
|
564
|
+
// pickers already carry.
|
|
565
|
+
function DataPhoneEditor({ col, value, isPending, onCommit, }) {
|
|
566
|
+
const parsed = parsePhoneValue(value);
|
|
567
|
+
const [iso2, setIso2] = React.useState(parsed?.iso2 ?? "US");
|
|
568
|
+
const [digits, setDigits] = React.useState(parsed?.digits ?? "");
|
|
569
|
+
const country = countryByIso2(iso2);
|
|
570
|
+
const countryOptions = COUNTRIES.map((c) => ({
|
|
571
|
+
value: c.iso2,
|
|
572
|
+
label: `${flagEmoji(c.iso2)} ${c.name} +${c.dialCode}`,
|
|
573
|
+
keywords: `${c.name} ${c.iso2} ${c.dialCode}`,
|
|
574
|
+
}));
|
|
575
|
+
const commit = (nextIso2, nextDigits) => onCommit(`${nextIso2}|${nextDigits}`);
|
|
576
|
+
return (_jsxs("span", { className: "crisp-datatable-phone-editor", children: [_jsx(Select, { variant: "ghost", searchable: true, "aria-label": `Edit ${col.key} country`, value: iso2, disabled: isPending, options: countryOptions, onValueChange: (v) => {
|
|
577
|
+
setIso2(v);
|
|
578
|
+
commit(v, digits);
|
|
579
|
+
} }), _jsxs("span", { className: "crisp-datatable-phone-dial", "aria-hidden": "true", children: ["+", country?.dialCode ?? "?"] }), _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) => {
|
|
580
|
+
if (e.key === "Enter")
|
|
581
|
+
commit(iso2, digits);
|
|
582
|
+
} })] }));
|
|
583
|
+
}
|
|
584
|
+
// crisp/#762, confirmed live -- Attio's own Timestamp editor is a calendar +
|
|
585
|
+
// time + preset picker, not the native `datetime-local` input `type:
|
|
586
|
+
// "datetime"` gets by default (that stays the DEFAULT, unchanged, for
|
|
587
|
+
// backward compatibility -- this is a new, opt-in `editor: "datetime"`).
|
|
588
|
+
// Composes crisp's own `DatePickerField` (calendar + presets, already the
|
|
589
|
+
// measured shape: Today/Tomorrow/Next week/No date chips under the
|
|
590
|
+
// calendar) with `TimePicker` (hour/minute wheel) side by side. The exact
|
|
591
|
+
// PRESET WORDING/order and the time control's own wheel-vs-text-field
|
|
592
|
+
// shape weren't independently re-verified against Attio's pixels this
|
|
593
|
+
// pass (time-boxed) -- `TimePicker` is crisp's only time-input primitive,
|
|
594
|
+
// reused rather than inventing a new one from scratch.
|
|
595
|
+
function DataDateTimeEditor({ col, value, isPending, onCommit, }) {
|
|
596
|
+
const initial = value instanceof Date
|
|
597
|
+
? value
|
|
598
|
+
: typeof value === "string" || typeof value === "number"
|
|
599
|
+
? new Date(value)
|
|
600
|
+
: null;
|
|
601
|
+
const hasInitial = initial != null && !Number.isNaN(initial.getTime());
|
|
602
|
+
const [date, setDate] = React.useState(hasInitial ? initial : null);
|
|
603
|
+
const [time, setTime] = React.useState({
|
|
604
|
+
hour: hasInitial ? initial.getHours() : 0,
|
|
605
|
+
minute: hasInitial ? initial.getMinutes() : 0,
|
|
606
|
+
});
|
|
607
|
+
const commit = (nextDate, nextTime) => {
|
|
608
|
+
if (!nextDate) {
|
|
609
|
+
onCommit("");
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
613
|
+
onCommit(`${nextDate.getFullYear()}-${pad(nextDate.getMonth() + 1)}-${pad(nextDate.getDate())}T${pad(nextTime.hour)}:${pad(nextTime.minute)}`);
|
|
614
|
+
};
|
|
615
|
+
const preset = (label, days) => ({
|
|
616
|
+
label,
|
|
617
|
+
date: () => {
|
|
618
|
+
const d = new Date();
|
|
619
|
+
d.setDate(d.getDate() + days);
|
|
620
|
+
return d;
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
return (_jsxs("span", { className: "crisp-datatable-datetime-editor", children: [_jsx(DatePickerField, { "aria-label": `Edit ${col.key} date`, variant: "ghost",
|
|
624
|
+
// "No date" chip in Attio's own popover maps to DatePickerField's
|
|
625
|
+
// existing Clear affordance -- its own default label ("Clear
|
|
626
|
+
// date") is already overridable via `useCrispMessage` internally,
|
|
627
|
+
// so no hardcoded copy of this component's own is added here.
|
|
628
|
+
value: date, presets: [
|
|
629
|
+
preset("Today", 0),
|
|
630
|
+
preset("Tomorrow", 1),
|
|
631
|
+
preset("Next week", 7),
|
|
632
|
+
], onValueChange: (d) => {
|
|
633
|
+
setDate(d);
|
|
634
|
+
commit(d, time);
|
|
635
|
+
} }), _jsx(TimePicker, { size: "small", value: time, disabled: isPending, onValueChange: (t) => {
|
|
636
|
+
setTime(t);
|
|
637
|
+
commit(date, t);
|
|
638
|
+
} })] }));
|
|
639
|
+
}
|
|
533
640
|
export 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 }) {
|
|
534
641
|
// crisp/#471: same resolution order as TableToolbar's own — an explicit
|
|
535
642
|
// prop wins, then the provider's translation of the built-in English
|
|
@@ -1033,14 +1140,48 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1033
1140
|
const colAriaIndex = (i) => ariaColIndexBase + i + 1;
|
|
1034
1141
|
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id));
|
|
1035
1142
|
const someSelected = selected.size > 0 && !allSelected;
|
|
1143
|
+
// crisp/#782, ported from RecordsTable's own already-shipped logic: the
|
|
1144
|
+
// header checkbox speaks for the rows you can SEE. Rebuilding the whole
|
|
1145
|
+
// set from `rows` (the filtered/visible set) would silently drop any
|
|
1146
|
+
// selection an active filter is hiding -- add/remove only the visible
|
|
1147
|
+
// rows and leave the rest of `selected` alone.
|
|
1036
1148
|
function toggleAll() {
|
|
1037
|
-
|
|
1149
|
+
const next = new Set(selected);
|
|
1150
|
+
for (const r of rows) {
|
|
1151
|
+
if (allSelected)
|
|
1152
|
+
next.delete(r.id);
|
|
1153
|
+
else
|
|
1154
|
+
next.add(r.id);
|
|
1155
|
+
}
|
|
1156
|
+
setSelected(next);
|
|
1038
1157
|
}
|
|
1039
1158
|
function toggleRow(id) {
|
|
1040
1159
|
const next = new Set(selected);
|
|
1041
1160
|
next.has(id) ? next.delete(id) : next.add(id);
|
|
1042
1161
|
setSelected(next);
|
|
1043
1162
|
}
|
|
1163
|
+
// crisp/#781: Shift+Click on a row checkbox selects the whole contiguous
|
|
1164
|
+
// range from the last plain click (the anchor) to this row -- confirmed
|
|
1165
|
+
// live, the mouse counterpart to #749's Shift+Arrow keyboard gesture,
|
|
1166
|
+
// sharing the same `rowSelectAnchor` state and the same recompute-the-
|
|
1167
|
+
// whole-span shape (not accumulate) so shift-clicking back toward the
|
|
1168
|
+
// anchor shrinks the same way clicking away from it grows. A plain click
|
|
1169
|
+
// both toggles normally AND re-anchors, so the NEXT Shift+Click extends
|
|
1170
|
+
// from wherever the user last plain-clicked, matching Attio.
|
|
1171
|
+
function toggleRowOrExtend(id, shiftKey) {
|
|
1172
|
+
if (shiftKey && rowSelectAnchor != null) {
|
|
1173
|
+
const anchorIdx = rows.findIndex((r) => r.id === rowSelectAnchor);
|
|
1174
|
+
const idx = rows.findIndex((r) => r.id === id);
|
|
1175
|
+
if (anchorIdx >= 0 && idx >= 0) {
|
|
1176
|
+
const lo = Math.min(anchorIdx, idx);
|
|
1177
|
+
const hi = Math.max(anchorIdx, idx);
|
|
1178
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((r) => r.id)));
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
setRowSelectAnchor(id);
|
|
1183
|
+
toggleRow(id);
|
|
1184
|
+
}
|
|
1044
1185
|
function cycleSort(key) {
|
|
1045
1186
|
// Clicking a header drives a single sort, as before. Multi-sort is supplied
|
|
1046
1187
|
// by the caller through the `sort` prop (built in a sort popover).
|
|
@@ -1247,6 +1388,52 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1247
1388
|
setSelected(new Set(rows.map((rw) => rw.id)));
|
|
1248
1389
|
return;
|
|
1249
1390
|
}
|
|
1391
|
+
// crisp/#772: Cmd/Ctrl+ArrowUp jumps the selected cell to the first row,
|
|
1392
|
+
// Cmd/Ctrl+ArrowDown to the last -- confirmed live (Attio's own help
|
|
1393
|
+
// drawer documents "Jump to top"/"Jump to bottom" on these exact
|
|
1394
|
+
// bindings). `!e.shiftKey` guards this from stealing Cmd+Shift+Arrow,
|
|
1395
|
+
// the separate #773 gesture (extend-selection-to-first/last) handled
|
|
1396
|
+
// in its own branch right below.
|
|
1397
|
+
if ((e.metaKey || e.ctrlKey) &&
|
|
1398
|
+
!e.shiftKey &&
|
|
1399
|
+
(e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
1400
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1401
|
+
if (r0 < 0)
|
|
1402
|
+
return;
|
|
1403
|
+
e.preventDefault();
|
|
1404
|
+
const nr0 = e.key === "ArrowUp" ? 0 : rows.length - 1;
|
|
1405
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1406
|
+
clipboardProxyRef.current?.select();
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
// crisp/#773: Cmd/Ctrl+Shift+Up/Down extends the row-checkbox selection
|
|
1410
|
+
// all the way to the first/last row in one keystroke -- confirmed live,
|
|
1411
|
+
// the "jump" sibling of #749's one-step Shift+Arrow below, sharing the
|
|
1412
|
+
// same anchor→destination-span recompute (only the destination differs:
|
|
1413
|
+
// 0 / rows.length-1 instead of r±1). Checked ahead of #749's own branch
|
|
1414
|
+
// so the modifier routes here instead of being swallowed as a one-step
|
|
1415
|
+
// Shift+Arrow.
|
|
1416
|
+
if (selectable &&
|
|
1417
|
+
(e.metaKey || e.ctrlKey) &&
|
|
1418
|
+
e.shiftKey &&
|
|
1419
|
+
(e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
1420
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1421
|
+
if (r0 < 0)
|
|
1422
|
+
return;
|
|
1423
|
+
const nr0 = e.key === "ArrowDown" ? rows.length - 1 : 0;
|
|
1424
|
+
e.preventDefault();
|
|
1425
|
+
const anchorId = rowSelectAnchor ?? selCell.id;
|
|
1426
|
+
const anchorIdx = rows.findIndex((row) => row.id === anchorId);
|
|
1427
|
+
if (anchorIdx < 0)
|
|
1428
|
+
return;
|
|
1429
|
+
setRowSelectAnchor(anchorId);
|
|
1430
|
+
const lo = Math.min(anchorIdx, nr0);
|
|
1431
|
+
const hi = Math.max(anchorIdx, nr0);
|
|
1432
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((row) => row.id)));
|
|
1433
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1434
|
+
clipboardProxyRef.current?.select();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1250
1437
|
// crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
|
|
1251
1438
|
// confirmed live, distinct from Shift+Click's cell-range gesture above
|
|
1252
1439
|
// (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
|
|
@@ -1277,12 +1464,38 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1277
1464
|
clipboardProxyRef.current?.select();
|
|
1278
1465
|
return;
|
|
1279
1466
|
}
|
|
1467
|
+
// crisp/#770: Shift+Space toggles the current row's own checkbox without
|
|
1468
|
+
// moving the selected cell or touching any other row -- distinct from
|
|
1469
|
+
// #749/#773 above, whose Shift+Arrow gestures move the cursor as they
|
|
1470
|
+
// extend the range.
|
|
1471
|
+
if (selectable && e.shiftKey && e.key === " ") {
|
|
1472
|
+
e.preventDefault();
|
|
1473
|
+
toggleRow(selCell.id);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
// crisp/#771: Cmd/Ctrl+Enter always opens the row's record page,
|
|
1477
|
+
// regardless of whether the current column is editable -- distinct from
|
|
1478
|
+
// plain Enter below, which opens the cell's own editor first when one
|
|
1479
|
+
// exists.
|
|
1480
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
1481
|
+
if (onRowClick) {
|
|
1482
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1483
|
+
if (r0 < 0)
|
|
1484
|
+
return;
|
|
1485
|
+
e.preventDefault();
|
|
1486
|
+
onRowClick(rows[r0]);
|
|
1487
|
+
}
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1280
1490
|
const r = rows.findIndex((row) => row.id === selCell.id);
|
|
1281
1491
|
const c = columns.findIndex((col) => col.key === selCell.key);
|
|
1282
1492
|
if (r < 0 || c < 0)
|
|
1283
1493
|
return;
|
|
1284
1494
|
let nr = r;
|
|
1285
1495
|
let nc = c;
|
|
1496
|
+
// #772/#773 (Cmd/Ctrl±Shift+Up/Down) are both handled in their own
|
|
1497
|
+
// early-return branches above, before `r`/`c` are even computed -- a
|
|
1498
|
+
// plain arrow (no modifier) is the only thing that reaches here.
|
|
1286
1499
|
if (e.key === "ArrowUp")
|
|
1287
1500
|
nr = Math.max(0, r - 1);
|
|
1288
1501
|
else if (e.key === "ArrowDown")
|
|
@@ -1294,6 +1507,33 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1294
1507
|
const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
1295
1508
|
nc = forward ? Math.min(columns.length - 1, c + 1) : Math.max(0, c - 1);
|
|
1296
1509
|
}
|
|
1510
|
+
// crisp/#769: Tab/Shift+Tab move to the next/previous cell in reading
|
|
1511
|
+
// order, wrapping to the next (or previous) row at a row boundary. This
|
|
1512
|
+
// is pure logical/DOM order, not a visual direction, so unlike Left/Right
|
|
1513
|
+
// above it needs no RTL handling -- native tab order doesn't flip under
|
|
1514
|
+
// RTL either. At the grid's first/last cell there's nowhere left to go:
|
|
1515
|
+
// return without `preventDefault`, so Tab still falls through to the
|
|
1516
|
+
// browser's native focus order instead of getting trapped.
|
|
1517
|
+
else if (e.key === "Tab" && !e.shiftKey) {
|
|
1518
|
+
if (c + 1 < columns.length)
|
|
1519
|
+
nc = c + 1;
|
|
1520
|
+
else if (r + 1 < rows.length) {
|
|
1521
|
+
nr = r + 1;
|
|
1522
|
+
nc = 0;
|
|
1523
|
+
}
|
|
1524
|
+
else
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
else if (e.key === "Tab" && e.shiftKey) {
|
|
1528
|
+
if (c - 1 >= 0)
|
|
1529
|
+
nc = c - 1;
|
|
1530
|
+
else if (r - 1 >= 0) {
|
|
1531
|
+
nr = r - 1;
|
|
1532
|
+
nc = columns.length - 1;
|
|
1533
|
+
}
|
|
1534
|
+
else
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1297
1537
|
// crisp/#607, G-066/G-068: End jumps to the last row (confirmed live --
|
|
1298
1538
|
// it also triggers Attio's own lazy-load, which is a data-loading
|
|
1299
1539
|
// concern outside this component; the #577 scroll-into-view effect
|
|
@@ -1398,15 +1638,21 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1398
1638
|
}
|
|
1399
1639
|
setEditing(null);
|
|
1400
1640
|
}
|
|
1641
|
+
const reduceMotion = usePrefersReducedMotion();
|
|
1401
1642
|
// crisp/#577 -- keyboard nav to an off-screen (virtualized-out) cell
|
|
1402
|
-
// scrolls it into the rendered window, on both axes.
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
//
|
|
1409
|
-
//
|
|
1643
|
+
// scrolls it into the rendered window, on both axes. `renderItems`/
|
|
1644
|
+
// `rowOffsets` are computed unconditionally (`height == null` just makes
|
|
1645
|
+
// them a no-op elsewhere, see the comment above `renderItems`), so this
|
|
1646
|
+
// runs on both axes regardless of `height` too (crisp/#780 -- it used to
|
|
1647
|
+
// gate the vertical half on `height != null`, on the reasoning that only
|
|
1648
|
+
// `height` gives the grid an internal scroll position to move; that's
|
|
1649
|
+
// wrong when the grid's own container still scrolls without an explicit
|
|
1650
|
+
// height, e.g. inside a shorter ancestor, matching `RecordsTable`'s own
|
|
1651
|
+
// already-shipped #257 fix, which had no such gate). `widths` is read only
|
|
1652
|
+
// indirectly, through `colWidth`'s closure over it -- biome can't see
|
|
1653
|
+
// that, but a column resize while a cell is selected genuinely needs to
|
|
1654
|
+
// re-run this to keep it in view, so the dependency is real even though
|
|
1655
|
+
// the lint rule can't prove it.
|
|
1410
1656
|
// biome-ignore lint/correctness/useExhaustiveDependencies: widths is read via colWidth's closure, see comment above
|
|
1411
1657
|
React.useEffect(() => {
|
|
1412
1658
|
if (!selCell)
|
|
@@ -1414,28 +1660,37 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1414
1660
|
const el = scrollRef.current;
|
|
1415
1661
|
if (!el)
|
|
1416
1662
|
return;
|
|
1417
|
-
//
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1663
|
+
// `el.scrollTo` rather than a direct `el.scrollTop =`/`el.scrollLeft =`
|
|
1664
|
+
// assignment (crisp/#780, matching RecordsTable's own #208/#257 rule):
|
|
1665
|
+
// property assignment is ALWAYS instant, so it can never honour a
|
|
1666
|
+
// consumer's own `scroll-behavior: smooth` on this container; `scrollTo`
|
|
1667
|
+
// defers to that CSS when no `behavior` is given, and only forces
|
|
1668
|
+
// `"auto"` under `prefers-reduced-motion`, which is the one case that
|
|
1669
|
+
// must win even over a host page's own smooth-scroll opt-in.
|
|
1670
|
+
const scrollTo = (opts) => el.scrollTo({
|
|
1671
|
+
...opts,
|
|
1672
|
+
...(reduceMotion ? { behavior: "auto" } : null),
|
|
1673
|
+
});
|
|
1674
|
+
// Sets `viewport` directly from the SAME value just passed to
|
|
1675
|
+
// `scrollTo`, rather than waiting for a follow-up `scroll` event to
|
|
1676
|
+
// round-trip through the `onScroll` handler above -- a real browser
|
|
1677
|
+
// does fire one when the scroll position changes programmatically, but
|
|
1678
|
+
// relying on that would make this effect's own visible result (the
|
|
1422
1679
|
// newly-selected cell actually rendering) depend on a second render
|
|
1423
1680
|
// triggered from outside React's own update, which is both slower and
|
|
1424
1681
|
// one more thing that has to be true for this to work.
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
setViewport((v) => ({ ...v, top: newTop }));
|
|
1438
|
-
}
|
|
1682
|
+
const itemIndex = rowIdToItemIndex.get(selCell.id);
|
|
1683
|
+
if (itemIndex != null) {
|
|
1684
|
+
const itemTop = rowOffsets[itemIndex];
|
|
1685
|
+
const itemBottom = itemTop + renderItems[itemIndex].height;
|
|
1686
|
+
if (itemTop < el.scrollTop) {
|
|
1687
|
+
scrollTo({ top: itemTop });
|
|
1688
|
+
setViewport((v) => ({ ...v, top: itemTop }));
|
|
1689
|
+
}
|
|
1690
|
+
else if (itemBottom > el.scrollTop + el.clientHeight) {
|
|
1691
|
+
const newTop = itemBottom - el.clientHeight;
|
|
1692
|
+
scrollTo({ top: newTop });
|
|
1693
|
+
setViewport((v) => ({ ...v, top: newTop }));
|
|
1439
1694
|
}
|
|
1440
1695
|
}
|
|
1441
1696
|
const ci = columns.findIndex((c) => c.key === selCell.key);
|
|
@@ -1445,24 +1700,24 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1445
1700
|
left += colWidth(columns[i]);
|
|
1446
1701
|
const right = left + colWidth(columns[ci]);
|
|
1447
1702
|
if (left < el.scrollLeft) {
|
|
1448
|
-
|
|
1703
|
+
scrollTo({ left });
|
|
1449
1704
|
setViewport((v) => ({ ...v, left }));
|
|
1450
1705
|
}
|
|
1451
1706
|
else if (right > el.scrollLeft + el.clientWidth) {
|
|
1452
1707
|
const newLeft = right - el.clientWidth;
|
|
1453
|
-
|
|
1708
|
+
scrollTo({ left: newLeft });
|
|
1454
1709
|
setViewport((v) => ({ ...v, left: newLeft }));
|
|
1455
1710
|
}
|
|
1456
1711
|
}
|
|
1457
1712
|
}, [
|
|
1458
1713
|
selCell,
|
|
1459
|
-
height,
|
|
1460
1714
|
columns,
|
|
1461
1715
|
widths,
|
|
1462
1716
|
selectable,
|
|
1463
1717
|
rowIdToItemIndex,
|
|
1464
1718
|
rowOffsets,
|
|
1465
1719
|
renderItems,
|
|
1720
|
+
reduceMotion,
|
|
1466
1721
|
]);
|
|
1467
1722
|
const totalCols = columns.length + (selectable ? 1 : 0) + (showGrip ? 1 : 0);
|
|
1468
1723
|
const hasSummary = columns.some((c) => c.summary || c.calc);
|
|
@@ -1836,12 +2091,33 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1836
2091
|
}, onDragEnd: () => {
|
|
1837
2092
|
setDragRowId(null);
|
|
1838
2093
|
setDropRowId(null);
|
|
1839
|
-
}, children: _jsx(GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && (_jsx(TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: _jsx("span", { className: "crisp-datatable-check", children: _jsx(Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
2094
|
+
}, children: _jsx(GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && (_jsx(TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: _jsx("span", { className: "crisp-datatable-check", children: _jsx(Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
2095
|
+
// preventDefault stops the native check/change
|
|
2096
|
+
// this click would otherwise also fire -- this
|
|
2097
|
+
// handler is the single source of truth so a
|
|
2098
|
+
// Shift+Click's range-select and a plain
|
|
2099
|
+
// click's toggle-and-re-anchor never race each
|
|
2100
|
+
// other (crisp/#781).
|
|
2101
|
+
onClick: (e) => {
|
|
2102
|
+
e.preventDefault();
|
|
2103
|
+
toggleRowOrExtend(row.id, e.shiftKey);
|
|
2104
|
+
}, onChange: () => { } }) }) })), colSlots.map((slot) => {
|
|
1840
2105
|
if (slot.type === "spacer") {
|
|
1841
2106
|
return (_jsx("td", { role: "presentation", "aria-hidden": "true", style: { padding: 0, border: "none" } }, `__spacer_${slot.side}`));
|
|
1842
2107
|
}
|
|
1843
2108
|
const col = columns[slot.i];
|
|
1844
2109
|
const i = slot.i;
|
|
2110
|
+
// crisp/#779, matching RecordsTable's own #231 fix:
|
|
2111
|
+
// column 0 is the row's own identity (its name), not
|
|
2112
|
+
// a value it holds -- a screen reader crossing the
|
|
2113
|
+
// row should read "Google, Employees, 182,000", not a
|
|
2114
|
+
// bare "182,000". Spread onto every TableCell this
|
|
2115
|
+
// column can render as (every editor branch, not just
|
|
2116
|
+
// the default one), since the row-identity semantics
|
|
2117
|
+
// don't depend on which editor is open.
|
|
2118
|
+
const rowHeaderProps = i === 0
|
|
2119
|
+
? { as: "th", scope: "row" }
|
|
2120
|
+
: undefined;
|
|
1845
2121
|
const isEditing = editing?.id === row.id && editing?.key === col.key;
|
|
1846
2122
|
const value = cellValue(col, row);
|
|
1847
2123
|
const sticky = dataStickyCol(i);
|
|
@@ -1864,14 +2140,14 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1864
2140
|
// right). See the `isEditing` block below for their
|
|
1865
2141
|
// actual editor.
|
|
1866
2142
|
if (col.editable && col.editor === "switch") {
|
|
1867
|
-
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2143
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1868
2144
|
}
|
|
1869
2145
|
// crisp/#760: `editor: "rating"` is the same always-
|
|
1870
2146
|
// live shape as `switch` above -- clicking a star IS
|
|
1871
2147
|
// the commit gesture, confirmed live.
|
|
1872
2148
|
if (col.editable && col.editor === "rating") {
|
|
1873
2149
|
const n = typeof value === "number" ? value : Number(value);
|
|
1874
|
-
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_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 && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2150
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_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 && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1875
2151
|
}
|
|
1876
2152
|
// crisp/#761: `editor: "checkbox"` is a THIRD always-
|
|
1877
2153
|
// live boolean shape, distinct from `switch` --
|
|
@@ -1880,13 +2156,15 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1880
2156
|
// toggle. Reuses crisp's own `Checkbox` (already the
|
|
1881
2157
|
// measured 16px/r6/brand-fill grid checkbox).
|
|
1882
2158
|
if (col.editable && col.editor === "checkbox") {
|
|
1883
|
-
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(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 && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2159
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(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 && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1884
2160
|
}
|
|
1885
2161
|
if (isEditing &&
|
|
1886
2162
|
(col.editor === "select" ||
|
|
1887
2163
|
col.editor === "reference" ||
|
|
1888
|
-
col.editor === "actor"
|
|
1889
|
-
|
|
2164
|
+
col.editor === "actor" ||
|
|
2165
|
+
col.editor === "phone" ||
|
|
2166
|
+
col.editor === "datetime")) {
|
|
2167
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
|
|
1890
2168
|
// Same as the plain text editor's own cell:
|
|
1891
2169
|
// opening/using the picker must not also fire
|
|
1892
2170
|
// the row's onRowClick. Escape is handled by
|
|
@@ -1897,10 +2175,10 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1897
2175
|
// portaled popover, so a handler here can't
|
|
1898
2176
|
// rely on the key event ever bubbling through
|
|
1899
2177
|
// this element (#453).
|
|
1900
|
-
onClick: (e) => e.stopPropagation(), children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? (_jsx(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : (_jsx(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
2178
|
+
onClick: (e) => e.stopPropagation(), children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? (_jsx(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : col.editor === "phone" ? (_jsx(DataPhoneEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : col.editor === "datetime" ? (_jsx(DataDateTimeEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : (_jsx(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1901
2179
|
}
|
|
1902
2180
|
if (isEditing) {
|
|
1903
|
-
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, onClick: (e) => e.stopPropagation(), children: [_jsx("input", { className: "crisp-datatable-edit",
|
|
2181
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, onClick: (e) => e.stopPropagation(), children: [_jsx("input", { className: "crisp-datatable-edit",
|
|
1904
2182
|
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
1905
2183
|
autoFocus: true, ...editorFor(col), "data-editor": editorFor(col).type, disabled: isPending, defaultValue: col.type === "date"
|
|
1906
2184
|
? toDateInputValue(value)
|
|
@@ -1959,7 +2237,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1959
2237
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1960
2238
|
expandedCell?.key === col.key;
|
|
1961
2239
|
const cellContent = (_jsxs(_Fragment, { children: [!isExpanded && rendered, errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && (_jsx("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }));
|
|
1962
|
-
return (_jsx(ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: _jsx(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
|
|
2240
|
+
return (_jsx(ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: _jsx(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: col.editable
|
|
1963
2241
|
? "crisp-datatable-editable"
|
|
1964
2242
|
: undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
|
|
1965
2243
|
? ""
|
|
@@ -7,7 +7,15 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
|
7
7
|
const config = defineRecipe({
|
|
8
8
|
name: "datatable",
|
|
9
9
|
base: `display: flex; flex-direction: column; gap: ${c.space["2.5"]}; font-family: ${c.font.sans};
|
|
10
|
-
|
|
10
|
+
/* The toolbar carried no horizontal inset of its own, so its content
|
|
11
|
+
(search box, sort/filter chips) sat flush at the table's outer edge
|
|
12
|
+
(x=0) while the header row's own th/td padding (table.recipe.ts,
|
|
13
|
+
#465) starts every column's text at 12px -- one row down, the SAME
|
|
14
|
+
grid's own header cell already begins at that inset. c.space["3"]
|
|
15
|
+
here just matches the row directly below it, measured live against
|
|
16
|
+
Attio (the sort/filter row's search chip lines up with the first
|
|
17
|
+
column header's text, not the grid's raw edge). */
|
|
18
|
+
& .crisp-datatable-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; min-height: 49px; border-top: 1px solid ${c.borderSubtle}; padding: 0 ${c.space["3"]}; }
|
|
11
19
|
& .crisp-datatable-search { position: relative; flex: 1; max-width: 280px; display: inline-flex; align-items: center; }
|
|
12
20
|
& .crisp-datatable-search svg { position: absolute; inset-inline-start: ${c.space["2"]}; color: ${c.fgMuted}; pointer-events: none; }
|
|
13
21
|
& .crisp-datatable-search input { width: 100%; height: 32px; padding: 0 ${c.space["2"]} 0 ${c.space["6"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; outline: none; transition: box-shadow .15s; }
|
|
@@ -272,6 +280,15 @@ const config = defineRecipe({
|
|
|
272
280
|
link. */
|
|
273
281
|
& .crisp-datatable-link { color: ${c.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
274
282
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
283
|
+
/* phone cell type/editor (#765) -- measured live: the dial-code prefix
|
|
284
|
+
is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
|
|
285
|
+
value already measured elsewhere in this file. */
|
|
286
|
+
& .crisp-datatable-phone { display: inline-flex; align-items: center; gap: ${c.space["1"]}; }
|
|
287
|
+
& .crisp-datatable-phone-dial { color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
288
|
+
& .crisp-datatable-phone-editor { display: flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; }
|
|
289
|
+
& .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
|
|
290
|
+
/* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
|
|
291
|
+
& .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; }
|
|
275
292
|
/* rating cell type/editor (#760) -- measured live: 16px stars on an
|
|
276
293
|
18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
|
|
277
294
|
committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
|
|
@@ -122,6 +122,15 @@ export function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeC
|
|
|
122
122
|
const finalWidth = rect ? Math.round(rect.width) : width;
|
|
123
123
|
commitSize(finalWidth);
|
|
124
124
|
}
|
|
125
|
+
// crisp/#795: double-click resets to `defaultSize`, confirmed live on
|
|
126
|
+
// Attio's own record-page divider (dragged 722px -> 924px, double-click
|
|
127
|
+
// snapped it back to exactly 722px -- its own default/initial width, not
|
|
128
|
+
// some other fixed value). Reuses `commitSize`, the same commit path a
|
|
129
|
+
// drag already uses, so persistence (`storageKey`) picks up the reset the
|
|
130
|
+
// same way it picks up a drag -- no separate reset-specific write.
|
|
131
|
+
function onHandleDoubleClick() {
|
|
132
|
+
commitSize(defaultSize);
|
|
133
|
+
}
|
|
125
134
|
const maxStyle = max == null ? undefined : typeof max === "number" ? `${max}px` : max;
|
|
126
135
|
const panelSizeStyle = {
|
|
127
136
|
width,
|
|
@@ -130,6 +139,6 @@ export function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeC
|
|
|
130
139
|
};
|
|
131
140
|
return (_jsxs("div", { ref: containerRef, className: resizablepanelRecipe({ className }), ...rest, children: [_jsx("div", { className: "crisp-resizablepanel-spacer", style: floating ? { width: 0 } : { width, minWidth: min, maxWidth: maxStyle } }), _jsxs("div", { ref: panelRef, className: "crisp-resizablepanel-panel", "data-side": side, "data-floating": floating ? "true" : "false", style: panelSizeStyle, children: [_jsx("div", { className: "crisp-resizablepanel-inner", children: children }), _jsx("div", { className: handleVariant === "column"
|
|
132
141
|
? "crisp-resizablepanel-handle crisp-resizablepanel-handle--column"
|
|
133
|
-
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, children: handleVariant === "column" && (_jsx("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
142
|
+
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onDoubleClick: onHandleDoubleClick, children: handleVariant === "column" && (_jsx("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
134
143
|
}
|
|
135
144
|
ResizablePanel.displayName = "ResizablePanel";
|
|
@@ -28,7 +28,14 @@ export const TableRow = React.forwardRef((props, ref) => _jsx("tr", { ref: ref,
|
|
|
28
28
|
TableRow.displayName = "TableRow";
|
|
29
29
|
export const TableHead = React.forwardRef(({ align, ...props }, ref) => _jsx("th", { ref: ref, "data-align": align, ...props }));
|
|
30
30
|
TableHead.displayName = "TableHead";
|
|
31
|
-
export const TableCell = React.forwardRef(({ align, ...props }, ref) =>
|
|
31
|
+
export const TableCell = React.forwardRef(({ align, as, ...props }, ref) => {
|
|
32
|
+
// Cast to "td", same as RecordsTable's own dynamic-tag precedent --
|
|
33
|
+
// `props` (which may carry `scope`) would otherwise need to satisfy
|
|
34
|
+
// both <td> and <th>'s JSX attribute types at once for a bare union
|
|
35
|
+
// element name.
|
|
36
|
+
const Cell = (as ?? "td");
|
|
37
|
+
return _jsx(Cell, { ref: ref, "data-align": align, ...props });
|
|
38
|
+
});
|
|
32
39
|
TableCell.displayName = "TableCell";
|
|
33
40
|
export const TableCaption = React.forwardRef((props, ref) => _jsx("caption", { ref: ref, ...props }));
|
|
34
41
|
TableCaption.displayName = "TableCaption";
|