@crisp-ui-kit/crisp 0.54.6 → 0.54.8
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/authshell.css +5 -1
- package/dist/authshell.layered.css +5 -1
- package/dist/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/board.css +13 -9
- package/dist/board.layered.css +13 -9
- package/dist/button.css +3 -1
- package/dist/button.layered.css +3 -1
- package/dist/cjs/components/badge/badge.recipe.js +28 -5
- package/dist/cjs/components/button/button.js +33 -2
- package/dist/cjs/components/button/button.recipe.js +31 -5
- package/dist/cjs/components/datatable/datatable.js +66 -7
- package/dist/cjs/components/datatable/datatable.recipe.js +57 -2
- package/dist/cjs/components/dialog/dialog.js +8 -5
- package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +19 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +239 -50
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
- package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
- package/dist/cjs/components/tooltip/tooltip.js +23 -9
- package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/cjs/product/authshell/authshell.js +14 -1
- package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
- package/dist/cjs/product/board/board.js +122 -27
- package/dist/cjs/product/board/board.recipe.js +14 -2
- package/dist/cjs/product/comments/comments.js +23 -10
- package/dist/cjs/product/comments/comments.recipe.js +17 -3
- package/dist/cjs/product/formdialog/formdialog.js +10 -4
- package/dist/cjs/product/navitem/navitem.js +7 -2
- package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
- package/dist/cjs/product/objectsettings/index.js +1 -2
- package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
- package/dist/cjs/product/richtext/richtext.js +76 -1
- package/dist/cjs/product/sidebar/sidebar.js +68 -15
- package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/cjs/tokens/palette.js +32 -1
- package/dist/cjs/tokens/semantic.js +11 -0
- package/dist/comments.css +36 -10
- package/dist/comments.layered.css +36 -10
- package/dist/datatable.css +47 -9
- package/dist/datatable.layered.css +47 -9
- package/dist/dialog.css +13 -1
- package/dist/dialog.layered.css +13 -1
- package/dist/esm/components/badge/badge.recipe.js +28 -5
- package/dist/esm/components/button/button.js +34 -3
- package/dist/esm/components/button/button.recipe.js +31 -5
- package/dist/esm/components/datatable/datatable.js +67 -8
- package/dist/esm/components/datatable/datatable.recipe.js +57 -2
- package/dist/esm/components/dialog/dialog.js +8 -5
- package/dist/esm/components/dialog/dialog.recipe.js +15 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +19 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +240 -51
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
- package/dist/esm/components/tabs/tabs.recipe.js +11 -2
- package/dist/esm/components/tooltip/tooltip.js +23 -9
- package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/esm/product/authshell/authshell.js +15 -2
- package/dist/esm/product/authshell/authshell.recipe.js +13 -1
- package/dist/esm/product/authshell/index.js +1 -1
- package/dist/esm/product/board/board.js +122 -27
- package/dist/esm/product/board/board.recipe.js +14 -2
- package/dist/esm/product/comments/comments.js +23 -10
- package/dist/esm/product/comments/comments.recipe.js +17 -3
- package/dist/esm/product/formdialog/formdialog.js +10 -4
- package/dist/esm/product/navitem/index.js +1 -1
- package/dist/esm/product/navitem/navitem.js +7 -2
- package/dist/esm/product/navitem/navitem.recipe.js +16 -0
- package/dist/esm/product/objectsettings/index.js +1 -1
- package/dist/esm/product/objectsettings/objectsettings.js +2 -12
- package/dist/esm/product/richtext/richtext.js +76 -1
- package/dist/esm/product/sidebar/sidebar.js +69 -16
- package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/esm/tokens/palette.js +32 -1
- package/dist/esm/tokens/semantic.js +11 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +16 -2
- package/dist/navitem.css +7 -0
- package/dist/navitem.layered.css +7 -0
- package/dist/recordstable.css +2 -1
- package/dist/recordstable.layered.css +2 -1
- package/dist/sidebar.css +6 -1
- package/dist/sidebar.layered.css +6 -1
- package/dist/styles.css +165 -42
- package/dist/styles.layered.css +165 -42
- package/dist/tabletoolbar.css +17 -1
- package/dist/tabletoolbar.layered.css +17 -1
- package/dist/tabs.css +1 -1
- package/dist/tabs.layered.css +1 -1
- package/dist/tokens.css +9 -1
- package/dist/tokens.json +16 -2
- package/dist/tooltip.css +1 -1
- package/dist/tooltip.layered.css +1 -1
- package/dist/types/components/badge/badge.recipe.d.ts +5 -1
- package/dist/types/components/button/button.recipe.d.ts +18 -5
- package/dist/types/components/datatable/datatable.d.ts +9 -0
- package/dist/types/components/dialog/dialog.d.ts +25 -2
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +47 -5
- package/dist/types/components/tooltip/tooltip.d.ts +12 -4
- package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
- package/dist/types/product/authshell/authshell.d.ts +26 -2
- package/dist/types/product/authshell/index.d.ts +1 -1
- package/dist/types/product/board/board.d.ts +5 -4
- package/dist/types/product/comments/comments.d.ts +10 -2
- package/dist/types/product/formdialog/formdialog.d.ts +2 -1
- package/dist/types/product/navitem/index.d.ts +1 -1
- package/dist/types/product/navitem/navitem.d.ts +23 -0
- package/dist/types/product/objectsettings/index.d.ts +1 -1
- package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
- package/dist/types/product/sidebar/sidebar.d.ts +33 -5
- package/dist/types/tokens/contract.d.ts +4 -0
- package/dist/types/tokens/palette.d.ts +13 -1
- package/dist/types/tokens/semantic.d.ts +11 -0
- package/package.json +1 -1
|
@@ -41,6 +41,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
41
41
|
const lucide_react_1 = require("lucide-react");
|
|
42
42
|
const React = __importStar(require("react"));
|
|
43
43
|
const core_1 = require("../../core");
|
|
44
|
+
const datepicker_1 = require("../datepicker");
|
|
44
45
|
const menu_1 = require("../menu");
|
|
45
46
|
const select_1 = require("../select");
|
|
46
47
|
const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
|
|
@@ -52,13 +53,32 @@ const OP_LABELS = {
|
|
|
52
53
|
is: "is",
|
|
53
54
|
isnot: "is not",
|
|
54
55
|
isanyof: "is any of",
|
|
55
|
-
empty
|
|
56
|
-
|
|
56
|
+
// Attio's own text says "empty"/"not empty", not "is empty"/"is not empty"
|
|
57
|
+
// -- #619's own issue body quotes it verbatim this way, and #593's number/
|
|
58
|
+
// date operator sets ("is / is not / less than / greater than / empty /
|
|
59
|
+
// not empty") repeat it. #619 ported the 3 new ops but left these two
|
|
60
|
+
// untouched from the pre-existing 5-op set, which had drifted; caught in
|
|
61
|
+
// review.
|
|
62
|
+
empty: "empty",
|
|
63
|
+
notempty: "not empty",
|
|
64
|
+
lessthan: "less than",
|
|
65
|
+
greaterthan: "greater than",
|
|
66
|
+
before: "before",
|
|
67
|
+
after: "after",
|
|
57
68
|
};
|
|
58
69
|
// #619: order and label set match Attio's own text-filter operator picker
|
|
59
70
|
// verbatim (measured live, per RecordsTable's own #578 port of the same
|
|
60
71
|
// set: contains · does not contain · starts with · ends with · is · is not ·
|
|
61
|
-
// empty · not empty).
|
|
72
|
+
// empty · not empty). #593: number and date columns get their OWN shorter
|
|
73
|
+
// sets instead — also measured live (`app.attio.com`, a Twitter follower
|
|
74
|
+
// count / Foundation date column, each filter added then discarded): number
|
|
75
|
+
// is "is / is not / less than / greater than / empty / not empty" (104.86px
|
|
76
|
+
// wide, 6 rows); date is "is / is not / before / after / empty / not empty"
|
|
77
|
+
// (91.16px, 6 rows) — both narrower than the text set's own 156.5px, which
|
|
78
|
+
// is why `.crisp-tabletoolbar-menu--op`'s old fixed 134px floor is gone (see
|
|
79
|
+
// tabletoolbar.recipe.ts): every one of these three sets is real, live-
|
|
80
|
+
// measured content-driven width, and a floor sized for one of them
|
|
81
|
+
// overshoots the other two.
|
|
62
82
|
const OP_ORDER = [
|
|
63
83
|
"contains",
|
|
64
84
|
"doesnotcontain",
|
|
@@ -69,15 +89,44 @@ const OP_ORDER = [
|
|
|
69
89
|
"empty",
|
|
70
90
|
"notempty",
|
|
71
91
|
];
|
|
92
|
+
const NUMBER_OP_ORDER = [
|
|
93
|
+
"is",
|
|
94
|
+
"isnot",
|
|
95
|
+
"lessthan",
|
|
96
|
+
"greaterthan",
|
|
97
|
+
"empty",
|
|
98
|
+
"notempty",
|
|
99
|
+
];
|
|
100
|
+
const DATE_OP_ORDER = [
|
|
101
|
+
"is",
|
|
102
|
+
"isnot",
|
|
103
|
+
"before",
|
|
104
|
+
"after",
|
|
105
|
+
"empty",
|
|
106
|
+
"notempty",
|
|
107
|
+
];
|
|
72
108
|
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
73
|
-
// nothing to check off otherwise.
|
|
74
|
-
|
|
109
|
+
// nothing to check off otherwise. A column's `type` picks the base set
|
|
110
|
+
// first; "isanyof" can still follow any of the three (a column's own
|
|
111
|
+
// declared values aren't tied to its underlying number/date/text kind).
|
|
112
|
+
const opsFor = (col) => {
|
|
113
|
+
const base = col?.type === "number"
|
|
114
|
+
? NUMBER_OP_ORDER
|
|
115
|
+
: col?.type === "date"
|
|
116
|
+
? DATE_OP_ORDER
|
|
117
|
+
: OP_ORDER;
|
|
118
|
+
return col?.values?.length ? [...base, "isanyof"] : base;
|
|
119
|
+
};
|
|
75
120
|
const opNeedsValue = (op) => op === "contains" ||
|
|
76
121
|
op === "doesnotcontain" ||
|
|
77
122
|
op === "startswith" ||
|
|
78
123
|
op === "endswith" ||
|
|
79
124
|
op === "is" ||
|
|
80
|
-
op === "isnot"
|
|
125
|
+
op === "isnot" ||
|
|
126
|
+
op === "lessthan" ||
|
|
127
|
+
op === "greaterthan" ||
|
|
128
|
+
op === "before" ||
|
|
129
|
+
op === "after";
|
|
81
130
|
const opNeedsValues = (op) => op === "isanyof";
|
|
82
131
|
// "Department is any of IT, Finance" past two values becomes "… +N": the
|
|
83
132
|
// chip is one line, and a field can have far more than two facet values.
|
|
@@ -92,9 +141,10 @@ const asText = (v) => {
|
|
|
92
141
|
return v.join(", ");
|
|
93
142
|
// Before the generic object branch, which would look for `.name`, find none
|
|
94
143
|
// and return "" — turning every Date into the empty string (#185). The ISO
|
|
95
|
-
// date is stable and locale-independent, so `contains "2026-06"` works
|
|
96
|
-
//
|
|
97
|
-
//
|
|
144
|
+
// date is stable and locale-independent, so `contains "2026-06"` works and
|
|
145
|
+
// (#593) so does `is`/`is not` against a `type: "date"` column's own ISO
|
|
146
|
+
// condition value — `before`/`after` get a real chronological compare
|
|
147
|
+
// instead, below.
|
|
98
148
|
if (v instanceof Date)
|
|
99
149
|
return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
|
|
100
150
|
if (typeof v === "object") {
|
|
@@ -103,13 +153,67 @@ const asText = (v) => {
|
|
|
103
153
|
}
|
|
104
154
|
return String(v);
|
|
105
155
|
};
|
|
156
|
+
// #593: `lessthan`/`greaterthan` (number columns) and `before`/`after` (date
|
|
157
|
+
// columns) need a real numeric/chronological compare, not `asText`'s string
|
|
158
|
+
// one — `null` on anything that doesn't parse, so a condition against a
|
|
159
|
+
// malformed or missing value never matches rather than throwing.
|
|
160
|
+
const toNumberOrNull = (v) => {
|
|
161
|
+
if (typeof v === "number")
|
|
162
|
+
return Number.isNaN(v) ? null : v;
|
|
163
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
164
|
+
const n = Number(v);
|
|
165
|
+
return Number.isNaN(n) ? null : n;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
};
|
|
169
|
+
const toDateOrNull = (v) => {
|
|
170
|
+
if (v instanceof Date)
|
|
171
|
+
return Number.isNaN(v.getTime()) ? null : v;
|
|
172
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
173
|
+
const d = new Date(v);
|
|
174
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
178
|
+
// A `type: "date"` condition's own value is stored as a plain ISO date
|
|
179
|
+
// string ("2026-09-25") — machine-comparable (`before`/`after` above,
|
|
180
|
+
// `is`/`is not` via `asText`) and locale-independent, the same choice #185
|
|
181
|
+
// already made for `asText`'s own `Date` branch. Parsed/formatted at local
|
|
182
|
+
// midnight, not UTC, so the picked calendar day and the displayed day are
|
|
183
|
+
// always the same one regardless of timezone offset.
|
|
184
|
+
const isoToDate = (iso) => {
|
|
185
|
+
if (!iso)
|
|
186
|
+
return null;
|
|
187
|
+
const d = new Date(`${iso}T00:00:00`);
|
|
188
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
189
|
+
};
|
|
190
|
+
const dateToISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
191
|
+
// e.g. "Sep 25, 2026" — the same format `DatePickerField`'s own default
|
|
192
|
+
// uses, and what Attio's chip itself showed after picking "Today" live.
|
|
193
|
+
const formatFilterDate = (iso) => {
|
|
194
|
+
const d = isoToDate(iso);
|
|
195
|
+
return d
|
|
196
|
+
? d.toLocaleDateString(undefined, {
|
|
197
|
+
month: "short",
|
|
198
|
+
day: "numeric",
|
|
199
|
+
year: "numeric",
|
|
200
|
+
})
|
|
201
|
+
: iso;
|
|
202
|
+
};
|
|
203
|
+
const toDatePickerDate = (d) => ({
|
|
204
|
+
year: d.getFullYear(),
|
|
205
|
+
month: d.getMonth() + 1,
|
|
206
|
+
day: d.getDate(),
|
|
207
|
+
});
|
|
208
|
+
const fromDatePickerDate = (d) => new Date(d.year, d.month - 1, d.day);
|
|
106
209
|
/**
|
|
107
210
|
* Applies a toolbar's filter conditions to any row array. Reads the raw value
|
|
108
211
|
* at `row[key]` (override with `get` for derived fields) — never a formatted
|
|
109
212
|
* display string, so a grouped-thousands "$1,200" still matches "1200".
|
|
110
213
|
*/
|
|
111
214
|
function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
112
|
-
const active = filters.filter((f) =>
|
|
215
|
+
const active = filters.filter((f) => f.op != null &&
|
|
216
|
+
(!opNeedsValue(f.op) || f.value.trim() !== "") &&
|
|
113
217
|
(!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
|
|
114
218
|
if (!active.length)
|
|
115
219
|
return rows;
|
|
@@ -119,6 +223,20 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
|
119
223
|
return isEmptyValue(raw);
|
|
120
224
|
if (f.op === "notempty")
|
|
121
225
|
return !isEmptyValue(raw);
|
|
226
|
+
if (f.op === "lessthan" || f.op === "greaterthan") {
|
|
227
|
+
const n = toNumberOrNull(raw);
|
|
228
|
+
const cn = toNumberOrNull(f.value);
|
|
229
|
+
if (n == null || cn == null)
|
|
230
|
+
return false;
|
|
231
|
+
return f.op === "lessthan" ? n < cn : n > cn;
|
|
232
|
+
}
|
|
233
|
+
if (f.op === "before" || f.op === "after") {
|
|
234
|
+
const d = toDateOrNull(raw);
|
|
235
|
+
const cd = toDateOrNull(f.value);
|
|
236
|
+
if (d == null || cd == null)
|
|
237
|
+
return false;
|
|
238
|
+
return f.op === "before" ? d < cd : d > cd;
|
|
239
|
+
}
|
|
122
240
|
const t = asText(raw).toLowerCase();
|
|
123
241
|
if (f.op === "isanyof")
|
|
124
242
|
return (f.values ?? []).some((v) => v.toLowerCase() === t);
|
|
@@ -177,7 +295,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
177
295
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
178
296
|
* Created by), which one shared list can't reproduce.
|
|
179
297
|
*/
|
|
180
|
-
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
|
|
298
|
+
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
|
|
181
299
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
182
300
|
// CrispMessagesProvider's translation of the English default below, or
|
|
183
301
|
// that default itself with no provider — unchanged from before this
|
|
@@ -311,11 +429,29 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
311
429
|
};
|
|
312
430
|
const addFilter = (key) => {
|
|
313
431
|
const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
|
|
314
|
-
putFilters([...filters, { id, key,
|
|
432
|
+
putFilters([...filters, { id, key, value: "" }]);
|
|
315
433
|
setFilterQuery("");
|
|
316
|
-
|
|
434
|
+
// matches Attio: picking a field opens the operator menu next, not a
|
|
435
|
+
// combined dialog — measured live, 2026-09-25.
|
|
436
|
+
setOpenMenu(`fop:${id}`);
|
|
317
437
|
};
|
|
318
438
|
const patchFilter = (id, patch) => putFilters(filters.map((c) => (c.id === id ? { ...c, ...patch } : c)));
|
|
439
|
+
// The field segment's own picker (#462 follow-up): changing the field
|
|
440
|
+
// replaces it in place — confirmed live, one chip, not a second — and
|
|
441
|
+
// resets op/value/values, since neither is guaranteed to make sense for
|
|
442
|
+
// the new field's type (a text op on a select field, e.g.).
|
|
443
|
+
const changeFilterField = (id, key) => {
|
|
444
|
+
patchFilter(id, { key, op: undefined, value: "", values: undefined });
|
|
445
|
+
setFilterQuery("");
|
|
446
|
+
setOpenMenu(`fop:${id}`);
|
|
447
|
+
};
|
|
448
|
+
// The operator segment's own menu: picking an operator that needs a value
|
|
449
|
+
// auto-opens the value popover, focused; "empty"/"not empty" need nothing
|
|
450
|
+
// further, so the menu just closes — both measured live, 2026-09-25.
|
|
451
|
+
const pickOp = (id, op) => {
|
|
452
|
+
patchFilter(id, { op });
|
|
453
|
+
setOpenMenu(opNeedsValue(op) || opNeedsValues(op) ? `fval:${id}` : null);
|
|
454
|
+
};
|
|
319
455
|
const removeFilter = (id) => {
|
|
320
456
|
putFilters(filters.filter((c) => c.id !== id));
|
|
321
457
|
setOpenMenu(null);
|
|
@@ -354,6 +490,23 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
354
490
|
close: () => setOpenMenu(null),
|
|
355
491
|
contentRef: kebabMenuRef,
|
|
356
492
|
});
|
|
493
|
+
// The operator popover is `role="menu"` too — confirmed live, 2026-09-25 —
|
|
494
|
+
// so it gets the same shared keyboard model as the kebab above, one hook
|
|
495
|
+
// call tracking whichever condition's operator menu is open.
|
|
496
|
+
const opMenuRef = React.useRef(null);
|
|
497
|
+
const { onKeyDown: onOpKeyDown } = (0, menu_1.useMenuKeyboard)({
|
|
498
|
+
open: typeof openMenu === "string" && openMenu.startsWith("fop:"),
|
|
499
|
+
close: () => setOpenMenu(null),
|
|
500
|
+
contentRef: opMenuRef,
|
|
501
|
+
});
|
|
502
|
+
// Shared between the "Filter" trigger's own add-condition picker and each
|
|
503
|
+
// chip's field segment (#462 follow-up) — both search the same
|
|
504
|
+
// `filterColumns` list and differ only in what a pick does.
|
|
505
|
+
const filterFieldPicker = (onSelect) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
|
|
506
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
507
|
+
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
|
|
508
|
+
.filter((col) => col.label.toLowerCase().includes(filterQuery.trim().toLowerCase()))
|
|
509
|
+
.map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => onSelect(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }));
|
|
357
510
|
const activeFilters = filters.length;
|
|
358
511
|
// The default phrase only works in English word order, so a caller can
|
|
359
512
|
// replace the whole thing rather than fill a template — crisp/#471's
|
|
@@ -368,7 +521,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
368
521
|
: sorts.length
|
|
369
522
|
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
370
523
|
: sortLabel;
|
|
371
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
524
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
372
525
|
if (dragSort !== null)
|
|
373
526
|
e.preventDefault();
|
|
374
527
|
}, onDrop: () => {
|
|
@@ -390,47 +543,83 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
390
543
|
label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
|
|
391
544
|
keywords: descendingLabel,
|
|
392
545
|
},
|
|
393
|
-
], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
546
|
+
], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
394
547
|
? filterTriggerLabel(activeFilters)
|
|
395
548
|
: activeFilters
|
|
396
549
|
? `${filterLabel} · ${activeFilters}`
|
|
397
|
-
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.
|
|
398
|
-
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
399
|
-
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
|
|
400
|
-
.filter((col) => col.label
|
|
401
|
-
.toLowerCase()
|
|
402
|
-
.includes(filterQuery.trim().toLowerCase()))
|
|
403
|
-
.map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
|
|
550
|
+
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
|
|
404
551
|
const col = filterColumns.find((c) => c.key === cond.key);
|
|
405
|
-
const
|
|
552
|
+
const fieldOpen = openMenu === `ffield:${cond.id}`;
|
|
553
|
+
const opOpen = openMenu === `fop:${cond.id}`;
|
|
554
|
+
const valOpen = openMenu === `fval:${cond.id}`;
|
|
406
555
|
const kebabOpen = openMenu === `fkebab:${cond.id}`;
|
|
407
|
-
|
|
556
|
+
// Gates the field/value segments' background (white once the
|
|
557
|
+
// condition is usable, the measured `f6f7f7` otherwise) — NOT
|
|
558
|
+
// whether the value popover renders; see `showValueSeg` below.
|
|
559
|
+
const complete = cond.op != null &&
|
|
560
|
+
(!opNeedsValue(cond.op) || cond.value.trim()) &&
|
|
408
561
|
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
562
|
+
// No value segment at all for "empty"/"not empty" (nothing to
|
|
563
|
+
// show) or before an operator is even chosen — matches Attio,
|
|
564
|
+
// which never renders a third box in either case.
|
|
565
|
+
const showValueSeg = cond.op != null &&
|
|
566
|
+
(opNeedsValue(cond.op) || opNeedsValues(cond.op));
|
|
567
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--field", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": fieldOpen, onClick: () => setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`), children: [col?.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 14, strokeWidth: 1.75 }) : null, col?.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op
|
|
568
|
+
? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
|
|
569
|
+
: selectConditionLabel }), showValueSeg && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
|
|
570
|
+
? cond.values?.length
|
|
571
|
+
? isAnyOfSummary(cond.values)
|
|
572
|
+
: filterValuePlaceholder
|
|
573
|
+
: cond.value.trim()
|
|
574
|
+
? col?.type === "date"
|
|
575
|
+
? formatFilterDate(cond.value)
|
|
576
|
+
: cond.value
|
|
577
|
+
: filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
578
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
579
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
580
|
+
.filter((v) => v
|
|
581
|
+
.toLowerCase()
|
|
582
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
583
|
+
.map((v) => {
|
|
584
|
+
const checked = (cond.values ?? []).includes(v);
|
|
585
|
+
return ((0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
|
|
586
|
+
const set = new Set(cond.values ?? []);
|
|
587
|
+
if (next)
|
|
588
|
+
set.add(v);
|
|
589
|
+
else
|
|
590
|
+
set.delete(v);
|
|
591
|
+
// the column's own order, not click order —
|
|
592
|
+
// what the chip summary reads out
|
|
593
|
+
patchFilter(cond.id, {
|
|
594
|
+
values: col.values?.filter((x) => set.has(x)),
|
|
595
|
+
});
|
|
596
|
+
}, children: v }, v));
|
|
597
|
+
})] })) : col?.type === "date" ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-datepop", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker, { ...{
|
|
598
|
+
selectionMode: "single",
|
|
599
|
+
value: isoToDate(cond.value)
|
|
600
|
+
? toDatePickerDate(isoToDate(cond.value))
|
|
601
|
+
: undefined,
|
|
602
|
+
onValueChange: (next) => {
|
|
603
|
+
if (!next)
|
|
604
|
+
return;
|
|
605
|
+
patchFilter(cond.id, {
|
|
606
|
+
value: dateToISO(fromDatePickerDate(next)),
|
|
607
|
+
});
|
|
608
|
+
setOpenMenu(null);
|
|
609
|
+
},
|
|
610
|
+
} }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-datepop-presets", children: [
|
|
611
|
+
[crispMessages.Today ?? "Today", 0],
|
|
612
|
+
[crispMessages.Tomorrow ?? "Tomorrow", 1],
|
|
613
|
+
[crispMessages["Next week"] ?? "Next week", 7],
|
|
614
|
+
].map(([label, addDays]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-datepop-preset", onClick: () => {
|
|
615
|
+
const d = new Date();
|
|
616
|
+
d.setDate(d.getDate() + addDays);
|
|
617
|
+
patchFilter(cond.id, { value: dateToISO(d) });
|
|
618
|
+
setOpenMenu(null);
|
|
619
|
+
}, children: label }, label))) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
620
|
+
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
621
|
+
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
|
|
622
|
+
}), showViewSettings && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
|
|
434
623
|
if (openMenu === "viewsettings") {
|
|
435
624
|
setOpenMenu(null);
|
|
436
625
|
}
|
|
@@ -454,7 +643,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
454
643
|
moveAttr(dragAttr, i);
|
|
455
644
|
setDragAttr(null);
|
|
456
645
|
}, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
|
|
457
|
-
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
646
|
+
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
458
647
|
}
|
|
459
648
|
/**
|
|
460
649
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|
|
@@ -4,6 +4,49 @@ exports.tabletoolbarRecipe = void 0;
|
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
6
|
const tabletoolbar_chrome_1 = require("./tabletoolbar.chrome");
|
|
7
|
+
// The filter condition chip, TableToolbar-only (crisp/#462 follow-up,
|
|
8
|
+
// re-measured live 2026-09-25). Deliberately NOT added to the shared
|
|
9
|
+
// `tabletoolbar.chrome.ts` functions: those also render under RecordsTable's
|
|
10
|
+
// own `crisp-recordstable-` prefix, whose still-tested (84 cases) chip
|
|
11
|
+
// predates this re-measurement and keeps its original `.fchip`/`.fchip-main`
|
|
12
|
+
// markup (one combined field+op+value button, plus a small floating kebab)
|
|
13
|
+
// until RecordsTable is rebuilt onto DataTable + TableToolbar and inherits
|
|
14
|
+
// this one instead. New, disjoint class names here so nothing shared changes
|
|
15
|
+
// meaning under the older markup.
|
|
16
|
+
//
|
|
17
|
+
// Measured on Attio's own chip (`app.attio.com/<ws>/companies`, a text-field
|
|
18
|
+
// condition): 4 independently-clickable, edge-to-edge segments, each 28px
|
|
19
|
+
// tall with its own `inset 0 0 0 1px` ring — field (radius 8/0/0/8), operator
|
|
20
|
+
// (flat), value (flat), kebab (radius 0/8/8/0). Background is NOT fixed per
|
|
21
|
+
// segment: the field and value segments (the two that show the condition's
|
|
22
|
+
// own text) read `rgb(246,247,247)` while the condition has no value yet and
|
|
23
|
+
// flip to white the instant it does (typing a value, or picking an operator
|
|
24
|
+
// like "empty" that needs none, both trigger it); the operator and kebab
|
|
25
|
+
// segments (pure controls, no text of the condition's own) stay white
|
|
26
|
+
// throughout. Click targets, confirmed live: the field segment reopens the
|
|
27
|
+
// attribute picker and replaces this condition's field in place (not a
|
|
28
|
+
// second chip); the operator segment opens an operator-only menu (closing it
|
|
29
|
+
// after a pick, then auto-opening the value popover, focused, when that
|
|
30
|
+
// operator needs one — "empty"/"not empty" just close); the value segment
|
|
31
|
+
// opens a value-only popover/input, independent of the operator menu (Attio
|
|
32
|
+
// never combines the two, unlike crisp's older single dialog).
|
|
33
|
+
const filterChipChrome = `
|
|
34
|
+
& .crisp-tabletoolbar-fcond { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.row}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgPrimary}; white-space: nowrap; }
|
|
35
|
+
& .crisp-tabletoolbar-fcond-seg { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 100%; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: ${tokens_1.contract.bgSurfaceMuted}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDivider}; font-family: inherit; font-size: inherit; color: inherit; cursor: pointer; }
|
|
36
|
+
& .crisp-tabletoolbar-fcond-seg svg { flex-shrink: 0; }
|
|
37
|
+
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--field,
|
|
38
|
+
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: ${tokens_1.contract.bgSurface}; }
|
|
39
|
+
& .crisp-tabletoolbar-fcond-seg--field { border-radius: ${tokens_1.contract.radius.md} 0 0 ${tokens_1.contract.radius.md}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
40
|
+
& .crisp-tabletoolbar-fcond-seg--field svg { color: ${tokens_1.contract.fgMuted}; }
|
|
41
|
+
& .crisp-tabletoolbar-fcond-seg--op { background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; }
|
|
42
|
+
& .crisp-tabletoolbar-fcond-seg--val { font-weight: ${tokens_1.contract.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
|
|
43
|
+
& .crisp-tabletoolbar-fcond-seg--kebab { background: ${tokens_1.contract.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${tokens_1.contract.radius.md} ${tokens_1.contract.radius.md} 0; color: ${tokens_1.contract.fgMuted}; }
|
|
44
|
+
& .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${tokens_1.contract.fgPrimary}; }
|
|
45
|
+
& .crisp-tabletoolbar-menu--val { min-width: 134px; }
|
|
46
|
+
& .crisp-tabletoolbar-datepop { width: 300px; }
|
|
47
|
+
& .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["1"]}; }
|
|
48
|
+
& .crisp-tabletoolbar-datepop-preset { height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.chip}; background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.sm}; cursor: pointer; }
|
|
49
|
+
& .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }`;
|
|
7
50
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
8
51
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
9
52
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -20,6 +63,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
20
63
|
& * { box-sizing: border-box; }
|
|
21
64
|
${(0, tabletoolbar_chrome_1.toolbarChromeHead)("tabletoolbar")}
|
|
22
65
|
${(0, tabletoolbar_chrome_1.toolbarChromeBody)("tabletoolbar")}
|
|
66
|
+
${filterChipChrome}
|
|
23
67
|
`,
|
|
24
68
|
});
|
|
25
69
|
exports.tabletoolbarRecipe = (0, core_1.resolveRecipe)(config);
|
|
@@ -14,7 +14,16 @@ const tokens_1 = require("../../tokens");
|
|
|
14
14
|
// inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
|
|
15
15
|
// a 16px leading icon and an optional count badge. Measured from the live
|
|
16
16
|
// app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
|
|
17
|
-
// gap, 14/500 labels
|
|
17
|
+
// gap, 14/500 labels. The count badge (crisp#697, re-measured against a
|
|
18
|
+
// record's tab counts and a settings "Attributes 35" count) is 14px
|
|
19
|
+
// tall/wide-min, radius 4px (no scale token lands there -- a recipe-local
|
|
20
|
+
// literal, same precedent as this file's own icon/gap literals), 10/500/14
|
|
21
|
+
// text on a 4% `fgPrimary` tint (`rgba(0,0,0,.04)`) with a measured
|
|
22
|
+
// `inset 0 0 0 1px borderSubtle` ring, text `fgNeutralSoft` (rgb(80,81,85),
|
|
23
|
+
// the same token Badge's neutral tone uses, #696) -- was 16px/r6/11px/
|
|
24
|
+
// `fgMuted`/6% tint with no ring at all. The selected tab's own badge
|
|
25
|
+
// colour (`fgPrimary`) wasn't measured (the live Overview tab carries no
|
|
26
|
+
// count) and stays as-is.
|
|
18
27
|
//
|
|
19
28
|
// The panel's own `padding-top` (`c.space["8"]`, 32px) is measured
|
|
20
29
|
// separately, live, on a company record page (Overview tab): the gap from
|
|
@@ -45,7 +54,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
45
54
|
& .crisp-tabs-tab:hover:not([data-selected]) { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); color: ${tokens_1.contract.fgPrimary}; }
|
|
46
55
|
& .crisp-tabs-tab[data-selected] { color: ${tokens_1.contract.fgPrimary}; background: ${tokens_1.contract.bgSubtle}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; }
|
|
47
56
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
|
|
48
|
-
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width:
|
|
57
|
+
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft}; }
|
|
49
58
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${tokens_1.contract.fgPrimary}; }
|
|
50
59
|
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${tokens_1.contract.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
51
60
|
& .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${tokens_1.contract.space["8"]}; }
|
|
@@ -103,12 +103,24 @@ style: _style, ...rest }, forwardedRef) => {
|
|
|
103
103
|
if (!el)
|
|
104
104
|
return;
|
|
105
105
|
const r = el.getBoundingClientRect();
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
const placement = side === "right"
|
|
107
|
+
? "right"
|
|
108
|
+
: side === "bottom" || r.top < 48
|
|
109
|
+
? "bottom"
|
|
110
|
+
: "top";
|
|
111
|
+
setPos(placement === "right"
|
|
112
|
+
? {
|
|
113
|
+
// Measured (crisp#633): the bubble's left edge sits 8px past
|
|
114
|
+
// the trigger's right edge, vertically centred on it.
|
|
115
|
+
left: Math.round(r.right + 8),
|
|
116
|
+
top: Math.round(r.top + r.height / 2),
|
|
117
|
+
placement,
|
|
118
|
+
}
|
|
119
|
+
: {
|
|
120
|
+
left: Math.round(r.left + r.width / 2),
|
|
121
|
+
top: Math.round(placement === "bottom" ? r.bottom + 6 : r.top - 6),
|
|
122
|
+
placement,
|
|
123
|
+
});
|
|
112
124
|
}, [open, side]);
|
|
113
125
|
const openAfterDelay = () => {
|
|
114
126
|
clearTimeout(timer.current);
|
|
@@ -155,9 +167,11 @@ style: _style, ...rest }, forwardedRef) => {
|
|
|
155
167
|
(0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: tipId, role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)({ variant }), style: {
|
|
156
168
|
left: pos.left,
|
|
157
169
|
top: pos.top,
|
|
158
|
-
transform: pos.
|
|
159
|
-
? "translate(-50
|
|
160
|
-
:
|
|
170
|
+
transform: pos.placement === "right"
|
|
171
|
+
? "translate(0, -50%)"
|
|
172
|
+
: pos.placement === "bottom"
|
|
173
|
+
? "translate(-50%, 0)"
|
|
174
|
+
: "translate(-50%, -100%)",
|
|
161
175
|
}, children: content }), document.body);
|
|
162
176
|
if (asChild) {
|
|
163
177
|
// No wrapper at all: the child carries the hover/focus/Escape
|
|
@@ -16,14 +16,15 @@ const tokens_1 = require("../../tokens");
|
|
|
16
16
|
// just the wrong pair.
|
|
17
17
|
//
|
|
18
18
|
// crisp#563: this `boxShadow` is Attio's PLAIN tooltip (a truncated header
|
|
19
|
-
// label, measured on "Twitter follower count")
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
19
|
+
// label, measured on "Twitter follower count"). Re-measured live and
|
|
20
|
+
// corroborated against a second sample (hovering the same field a second
|
|
21
|
+
// time) per AUTHORING.md Lesson 22 — the value below is an inset ring plus
|
|
22
|
+
// two offset shadows, not the single offset shadow this recipe carried
|
|
23
|
+
// before. Attio has a SECOND, visually similar but structurally different
|
|
24
|
+
// tooltip for a reference-column header (icon + the referenced type's name
|
|
25
|
+
// after the label, crisp#580) with its own measured shadow, `variant:
|
|
26
|
+
// "reference"` below. Background, text colour, radius, padding and font are
|
|
27
|
+
// identical between the two measured samples; only the shadow differs.
|
|
27
28
|
const spec = (0, core_1.defineSpec)({
|
|
28
29
|
name: "tooltip",
|
|
29
30
|
base: {
|
|
@@ -40,7 +41,7 @@ const spec = (0, core_1.defineSpec)({
|
|
|
40
41
|
fontSize: tokens_1.contract.text.sm,
|
|
41
42
|
fontWeight: tokens_1.contract.weight.medium,
|
|
42
43
|
lineHeight: "1.45",
|
|
43
|
-
boxShadow: "0
|
|
44
|
+
boxShadow: "rgb(33,34,36) 0 0 0 1px inset, rgba(0,0,0,0.04) 0 0 2px 0, rgba(0,0,0,0.12) 0 1px 3px 0",
|
|
44
45
|
opacity: 1,
|
|
45
46
|
transition: "opacity 0.12s ease",
|
|
46
47
|
},
|