@crisp-ui-kit/crisp 0.55.0 → 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 +194 -8
- package/dist/cjs/components/datatable/datatable.recipe.js +18 -1
- 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 +194 -8
- package/dist/esm/components/datatable/datatable.recipe.js +18 -1
- package/dist/styles.css +142 -103
- package/dist/styles.layered.css +142 -103
- 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/package.json +1 -1
package/dist/badge.css
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
height: 20px; padding-inline: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; gap: 3px; border-radius: var(--crisp-radius-sm);
|
|
28
28
|
}
|
|
29
29
|
.crisp-badge--size_large {
|
|
30
|
-
height: var(--crisp-size-chip); padding-inline:
|
|
30
|
+
height: var(--crisp-size-chip); padding-block: var(--crisp-space-px); padding-inline-start: var(--crisp-space-1); padding-inline-end: var(--crisp-space-1-5); font-size: var(--crisp-text-lg); letter-spacing: -0.1px; border-radius: var(--crisp-radius-lg);
|
|
31
31
|
}
|
|
32
32
|
.crisp-badge--variant_soft {
|
|
33
33
|
|
package/dist/badge.layered.css
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
height: 20px; padding-inline: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; gap: 3px; border-radius: var(--crisp-radius-sm);
|
|
29
29
|
}
|
|
30
30
|
.crisp-badge--size_large {
|
|
31
|
-
height: var(--crisp-size-chip); padding-inline:
|
|
31
|
+
height: var(--crisp-size-chip); padding-block: var(--crisp-space-px); padding-inline-start: var(--crisp-space-1); padding-inline-end: var(--crisp-space-1-5); font-size: var(--crisp-text-lg); letter-spacing: -0.1px; border-radius: var(--crisp-radius-lg);
|
|
32
32
|
}
|
|
33
33
|
.crisp-badge--variant_soft {
|
|
34
34
|
|
|
@@ -106,15 +106,20 @@ const spec = (0, core_1.defineSpec)({
|
|
|
106
106
|
gap: "3px",
|
|
107
107
|
borderRadius: tokens_1.contract.radius.sm,
|
|
108
108
|
},
|
|
109
|
-
// crisp#
|
|
110
|
-
//
|
|
111
|
-
//
|
|
109
|
+
// crisp#805: measured live against Attio's real status/stage chip
|
|
110
|
+
// (Deals "Deal stage" column, All Deals table view) -- radius 9px, not
|
|
111
|
+
// `radius.chip`'s 7px, and asymmetric padding (room for a leading dot/
|
|
112
|
+
// icon: 1px top/bottom, 6px trailing, 4px leading), not a symmetric
|
|
113
|
+
// 9px. Height/font/tracking are unchanged (still not independently
|
|
114
|
+
// re-measured this round, per #696's own note).
|
|
112
115
|
large: {
|
|
113
116
|
height: tokens_1.contract.size.chip,
|
|
114
|
-
|
|
117
|
+
paddingBlock: tokens_1.contract.space.px,
|
|
118
|
+
paddingInlineStart: tokens_1.contract.space["1"],
|
|
119
|
+
paddingInlineEnd: tokens_1.contract.space["1.5"],
|
|
115
120
|
fontSize: tokens_1.contract.text.lg,
|
|
116
121
|
letterSpacing: "-0.1px",
|
|
117
|
-
borderRadius: tokens_1.contract.radius.
|
|
122
|
+
borderRadius: tokens_1.contract.radius.lg,
|
|
118
123
|
},
|
|
119
124
|
},
|
|
120
125
|
variant: { soft: {}, solid: {} },
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COUNTRIES = void 0;
|
|
4
|
+
exports.flagEmoji = flagEmoji;
|
|
5
|
+
exports.countryByIso2 = countryByIso2;
|
|
6
|
+
exports.COUNTRIES = [
|
|
7
|
+
{ iso2: "US", name: "United States", dialCode: "1" },
|
|
8
|
+
{ iso2: "CA", name: "Canada", dialCode: "1" },
|
|
9
|
+
{ iso2: "GB", name: "United Kingdom", dialCode: "44" },
|
|
10
|
+
{ iso2: "IE", name: "Ireland", dialCode: "353" },
|
|
11
|
+
{ iso2: "FR", name: "France", dialCode: "33" },
|
|
12
|
+
{ iso2: "DE", name: "Germany", dialCode: "49" },
|
|
13
|
+
{ iso2: "ES", name: "Spain", dialCode: "34" },
|
|
14
|
+
{ iso2: "PT", name: "Portugal", dialCode: "351" },
|
|
15
|
+
{ iso2: "IT", name: "Italy", dialCode: "39" },
|
|
16
|
+
{ iso2: "NL", name: "Netherlands", dialCode: "31" },
|
|
17
|
+
{ iso2: "BE", name: "Belgium", dialCode: "32" },
|
|
18
|
+
{ iso2: "CH", name: "Switzerland", dialCode: "41" },
|
|
19
|
+
{ iso2: "AT", name: "Austria", dialCode: "43" },
|
|
20
|
+
{ iso2: "SE", name: "Sweden", dialCode: "46" },
|
|
21
|
+
{ iso2: "NO", name: "Norway", dialCode: "47" },
|
|
22
|
+
{ iso2: "DK", name: "Denmark", dialCode: "45" },
|
|
23
|
+
{ iso2: "FI", name: "Finland", dialCode: "358" },
|
|
24
|
+
{ iso2: "PL", name: "Poland", dialCode: "48" },
|
|
25
|
+
{ iso2: "CZ", name: "Czechia", dialCode: "420" },
|
|
26
|
+
{ iso2: "GR", name: "Greece", dialCode: "30" },
|
|
27
|
+
{ iso2: "RO", name: "Romania", dialCode: "40" },
|
|
28
|
+
{ iso2: "HU", name: "Hungary", dialCode: "36" },
|
|
29
|
+
{ iso2: "UA", name: "Ukraine", dialCode: "380" },
|
|
30
|
+
{ iso2: "RU", name: "Russia", dialCode: "7" },
|
|
31
|
+
{ iso2: "TR", name: "Turkey", dialCode: "90" },
|
|
32
|
+
{ iso2: "IL", name: "Israel", dialCode: "972" },
|
|
33
|
+
{ iso2: "AE", name: "United Arab Emirates", dialCode: "971" },
|
|
34
|
+
{ iso2: "SA", name: "Saudi Arabia", dialCode: "966" },
|
|
35
|
+
{ iso2: "EG", name: "Egypt", dialCode: "20" },
|
|
36
|
+
{ iso2: "ZA", name: "South Africa", dialCode: "27" },
|
|
37
|
+
{ iso2: "NG", name: "Nigeria", dialCode: "234" },
|
|
38
|
+
{ iso2: "KE", name: "Kenya", dialCode: "254" },
|
|
39
|
+
{ iso2: "IN", name: "India", dialCode: "91" },
|
|
40
|
+
{ iso2: "PK", name: "Pakistan", dialCode: "92" },
|
|
41
|
+
{ iso2: "BD", name: "Bangladesh", dialCode: "880" },
|
|
42
|
+
{ iso2: "LK", name: "Sri Lanka", dialCode: "94" },
|
|
43
|
+
{ iso2: "NP", name: "Nepal", dialCode: "977" },
|
|
44
|
+
{ iso2: "CN", name: "China", dialCode: "86" },
|
|
45
|
+
{ iso2: "HK", name: "Hong Kong", dialCode: "852" },
|
|
46
|
+
{ iso2: "TW", name: "Taiwan", dialCode: "886" },
|
|
47
|
+
{ iso2: "JP", name: "Japan", dialCode: "81" },
|
|
48
|
+
{ iso2: "KR", name: "South Korea", dialCode: "82" },
|
|
49
|
+
{ iso2: "MN", name: "Mongolia", dialCode: "976" },
|
|
50
|
+
{ iso2: "VN", name: "Vietnam", dialCode: "84" },
|
|
51
|
+
{ iso2: "TH", name: "Thailand", dialCode: "66" },
|
|
52
|
+
{ iso2: "SG", name: "Singapore", dialCode: "65" },
|
|
53
|
+
{ iso2: "MY", name: "Malaysia", dialCode: "60" },
|
|
54
|
+
{ iso2: "ID", name: "Indonesia", dialCode: "62" },
|
|
55
|
+
{ iso2: "PH", name: "Philippines", dialCode: "63" },
|
|
56
|
+
{ iso2: "AU", name: "Australia", dialCode: "61" },
|
|
57
|
+
{ iso2: "NZ", name: "New Zealand", dialCode: "64" },
|
|
58
|
+
{ iso2: "MX", name: "Mexico", dialCode: "52" },
|
|
59
|
+
{ iso2: "BR", name: "Brazil", dialCode: "55" },
|
|
60
|
+
{ iso2: "AR", name: "Argentina", dialCode: "54" },
|
|
61
|
+
{ iso2: "CL", name: "Chile", dialCode: "56" },
|
|
62
|
+
{ iso2: "CO", name: "Colombia", dialCode: "57" },
|
|
63
|
+
{ iso2: "PE", name: "Peru", dialCode: "51" },
|
|
64
|
+
];
|
|
65
|
+
/** Two Unicode Regional Indicator Symbols spell the flag emoji for `iso2`. */
|
|
66
|
+
function flagEmoji(iso2) {
|
|
67
|
+
return [...iso2.toUpperCase()]
|
|
68
|
+
.map((ch) => String.fromCodePoint(0x1f1e6 - 65 + ch.charCodeAt(0)))
|
|
69
|
+
.join("");
|
|
70
|
+
}
|
|
71
|
+
function countryByIso2(iso2) {
|
|
72
|
+
return exports.COUNTRIES.find((c) => c.iso2 === iso2);
|
|
73
|
+
}
|
|
@@ -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
|
|
@@ -1323,12 +1430,8 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1323
1430
|
// Cmd/Ctrl+ArrowDown to the last -- confirmed live (Attio's own help
|
|
1324
1431
|
// drawer documents "Jump to top"/"Jump to bottom" on these exact
|
|
1325
1432
|
// bindings). `!e.shiftKey` guards this from stealing Cmd+Shift+Arrow,
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
// branch for the same reason: without the guard, Cmd+Shift+Arrow would
|
|
1329
|
-
// still reach it (that branch doesn't check metaKey/ctrlKey either),
|
|
1330
|
-
// just not do the "jump" part -- the guard here is what keeps the two
|
|
1331
|
-
// gestures from fighting over the same keys.
|
|
1433
|
+
// the separate #773 gesture (extend-selection-to-first/last) handled
|
|
1434
|
+
// in its own branch right below.
|
|
1332
1435
|
if ((e.metaKey || e.ctrlKey) &&
|
|
1333
1436
|
!e.shiftKey &&
|
|
1334
1437
|
(e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
@@ -1341,6 +1444,34 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1341
1444
|
clipboardProxyRef.current?.select();
|
|
1342
1445
|
return;
|
|
1343
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
|
+
}
|
|
1344
1475
|
// crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
|
|
1345
1476
|
// confirmed live, distinct from Shift+Click's cell-range gesture above
|
|
1346
1477
|
// (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
|
|
@@ -1371,12 +1502,38 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1371
1502
|
clipboardProxyRef.current?.select();
|
|
1372
1503
|
return;
|
|
1373
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
|
+
}
|
|
1374
1528
|
const r = rows.findIndex((row) => row.id === selCell.id);
|
|
1375
1529
|
const c = columns.findIndex((col) => col.key === selCell.key);
|
|
1376
1530
|
if (r < 0 || c < 0)
|
|
1377
1531
|
return;
|
|
1378
1532
|
let nr = r;
|
|
1379
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.
|
|
1380
1537
|
if (e.key === "ArrowUp")
|
|
1381
1538
|
nr = Math.max(0, r - 1);
|
|
1382
1539
|
else if (e.key === "ArrowDown")
|
|
@@ -1388,6 +1545,33 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1388
1545
|
const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
1389
1546
|
nc = forward ? Math.min(columns.length - 1, c + 1) : Math.max(0, c - 1);
|
|
1390
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
|
+
}
|
|
1391
1575
|
// crisp/#607, G-066/G-068: End jumps to the last row (confirmed live --
|
|
1392
1576
|
// it also triggers Attio's own lazy-load, which is a data-loading
|
|
1393
1577
|
// concern outside this component; the #577 scroll-into-view effect
|
|
@@ -2015,7 +2199,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
2015
2199
|
if (isEditing &&
|
|
2016
2200
|
(col.editor === "select" ||
|
|
2017
2201
|
col.editor === "reference" ||
|
|
2018
|
-
col.editor === "actor"
|
|
2202
|
+
col.editor === "actor" ||
|
|
2203
|
+
col.editor === "phone" ||
|
|
2204
|
+
col.editor === "datetime")) {
|
|
2019
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,
|
|
2020
2206
|
// Same as the plain text editor's own cell:
|
|
2021
2207
|
// opening/using the picker must not also fire
|
|
@@ -2027,7 +2213,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
2027
2213
|
// portaled popover, so a handler here can't
|
|
2028
2214
|
// rely on the key event ever bubbling through
|
|
2029
2215
|
// this element (#453).
|
|
2030
|
-
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));
|
|
2031
2217
|
}
|
|
2032
2218
|
if (isEditing) {
|
|
2033
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",
|
|
@@ -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
|