@crisp-ui-kit/crisp 0.54.7 → 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 +12 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +231 -48
- 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 +12 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +231 -48
- 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 +1 -1
- package/dist/recordstable.layered.css +1 -1
- package/dist/sidebar.css +6 -1
- package/dist/sidebar.layered.css +6 -1
- package/dist/styles.css +163 -42
- package/dist/styles.layered.css +163 -42
- package/dist/tabletoolbar.css +16 -1
- package/dist/tabletoolbar.layered.css +16 -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 +43 -3
- 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");
|
|
@@ -60,11 +61,24 @@ const OP_LABELS = {
|
|
|
60
61
|
// review.
|
|
61
62
|
empty: "empty",
|
|
62
63
|
notempty: "not empty",
|
|
64
|
+
lessthan: "less than",
|
|
65
|
+
greaterthan: "greater than",
|
|
66
|
+
before: "before",
|
|
67
|
+
after: "after",
|
|
63
68
|
};
|
|
64
69
|
// #619: order and label set match Attio's own text-filter operator picker
|
|
65
70
|
// verbatim (measured live, per RecordsTable's own #578 port of the same
|
|
66
71
|
// set: contains · does not contain · starts with · ends with · is · is not ·
|
|
67
|
-
// 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.
|
|
68
82
|
const OP_ORDER = [
|
|
69
83
|
"contains",
|
|
70
84
|
"doesnotcontain",
|
|
@@ -75,15 +89,44 @@ const OP_ORDER = [
|
|
|
75
89
|
"empty",
|
|
76
90
|
"notempty",
|
|
77
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
|
+
];
|
|
78
108
|
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
79
|
-
// nothing to check off otherwise.
|
|
80
|
-
|
|
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
|
+
};
|
|
81
120
|
const opNeedsValue = (op) => op === "contains" ||
|
|
82
121
|
op === "doesnotcontain" ||
|
|
83
122
|
op === "startswith" ||
|
|
84
123
|
op === "endswith" ||
|
|
85
124
|
op === "is" ||
|
|
86
|
-
op === "isnot"
|
|
125
|
+
op === "isnot" ||
|
|
126
|
+
op === "lessthan" ||
|
|
127
|
+
op === "greaterthan" ||
|
|
128
|
+
op === "before" ||
|
|
129
|
+
op === "after";
|
|
87
130
|
const opNeedsValues = (op) => op === "isanyof";
|
|
88
131
|
// "Department is any of IT, Finance" past two values becomes "… +N": the
|
|
89
132
|
// chip is one line, and a field can have far more than two facet values.
|
|
@@ -98,9 +141,10 @@ const asText = (v) => {
|
|
|
98
141
|
return v.join(", ");
|
|
99
142
|
// Before the generic object branch, which would look for `.name`, find none
|
|
100
143
|
// and return "" — turning every Date into the empty string (#185). The ISO
|
|
101
|
-
// date is stable and locale-independent, so `contains "2026-06"` works
|
|
102
|
-
//
|
|
103
|
-
//
|
|
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.
|
|
104
148
|
if (v instanceof Date)
|
|
105
149
|
return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
|
|
106
150
|
if (typeof v === "object") {
|
|
@@ -109,13 +153,67 @@ const asText = (v) => {
|
|
|
109
153
|
}
|
|
110
154
|
return String(v);
|
|
111
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);
|
|
112
209
|
/**
|
|
113
210
|
* Applies a toolbar's filter conditions to any row array. Reads the raw value
|
|
114
211
|
* at `row[key]` (override with `get` for derived fields) — never a formatted
|
|
115
212
|
* display string, so a grouped-thousands "$1,200" still matches "1200".
|
|
116
213
|
*/
|
|
117
214
|
function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
118
|
-
const active = filters.filter((f) =>
|
|
215
|
+
const active = filters.filter((f) => f.op != null &&
|
|
216
|
+
(!opNeedsValue(f.op) || f.value.trim() !== "") &&
|
|
119
217
|
(!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
|
|
120
218
|
if (!active.length)
|
|
121
219
|
return rows;
|
|
@@ -125,6 +223,20 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
|
125
223
|
return isEmptyValue(raw);
|
|
126
224
|
if (f.op === "notempty")
|
|
127
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
|
+
}
|
|
128
240
|
const t = asText(raw).toLowerCase();
|
|
129
241
|
if (f.op === "isanyof")
|
|
130
242
|
return (f.values ?? []).some((v) => v.toLowerCase() === t);
|
|
@@ -183,7 +295,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
183
295
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
184
296
|
* Created by), which one shared list can't reproduce.
|
|
185
297
|
*/
|
|
186
|
-
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 }) {
|
|
187
299
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
188
300
|
// CrispMessagesProvider's translation of the English default below, or
|
|
189
301
|
// that default itself with no provider — unchanged from before this
|
|
@@ -317,11 +429,29 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
317
429
|
};
|
|
318
430
|
const addFilter = (key) => {
|
|
319
431
|
const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
|
|
320
|
-
putFilters([...filters, { id, key,
|
|
432
|
+
putFilters([...filters, { id, key, value: "" }]);
|
|
321
433
|
setFilterQuery("");
|
|
322
|
-
|
|
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}`);
|
|
323
437
|
};
|
|
324
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
|
+
};
|
|
325
455
|
const removeFilter = (id) => {
|
|
326
456
|
putFilters(filters.filter((c) => c.id !== id));
|
|
327
457
|
setOpenMenu(null);
|
|
@@ -360,6 +490,23 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
360
490
|
close: () => setOpenMenu(null),
|
|
361
491
|
contentRef: kebabMenuRef,
|
|
362
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)))] }));
|
|
363
510
|
const activeFilters = filters.length;
|
|
364
511
|
// The default phrase only works in English word order, so a caller can
|
|
365
512
|
// replace the whole thing rather than fill a template — crisp/#471's
|
|
@@ -374,7 +521,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
374
521
|
: sorts.length
|
|
375
522
|
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
376
523
|
: sortLabel;
|
|
377
|
-
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) => {
|
|
378
525
|
if (dragSort !== null)
|
|
379
526
|
e.preventDefault();
|
|
380
527
|
}, onDrop: () => {
|
|
@@ -396,47 +543,83 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
396
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] })),
|
|
397
544
|
keywords: descendingLabel,
|
|
398
545
|
},
|
|
399
|
-
], "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
|
|
400
547
|
? filterTriggerLabel(activeFilters)
|
|
401
548
|
: activeFilters
|
|
402
549
|
? `${filterLabel} · ${activeFilters}`
|
|
403
|
-
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.
|
|
404
|
-
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
405
|
-
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
|
|
406
|
-
.filter((col) => col.label
|
|
407
|
-
.toLowerCase()
|
|
408
|
-
.includes(filterQuery.trim().toLowerCase()))
|
|
409
|
-
.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) => {
|
|
410
551
|
const col = filterColumns.find((c) => c.key === cond.key);
|
|
411
|
-
const
|
|
552
|
+
const fieldOpen = openMenu === `ffield:${cond.id}`;
|
|
553
|
+
const opOpen = openMenu === `fop:${cond.id}`;
|
|
554
|
+
const valOpen = openMenu === `fval:${cond.id}`;
|
|
412
555
|
const kebabOpen = openMenu === `fkebab:${cond.id}`;
|
|
413
|
-
|
|
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()) &&
|
|
414
561
|
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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: () => {
|
|
440
623
|
if (openMenu === "viewsettings") {
|
|
441
624
|
setOpenMenu(null);
|
|
442
625
|
}
|
|
@@ -460,7 +643,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
460
643
|
moveAttr(dragAttr, i);
|
|
461
644
|
setDragAttr(null);
|
|
462
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) ??
|
|
463
|
-
`${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] }) }));
|
|
464
647
|
}
|
|
465
648
|
/**
|
|
466
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
|
},
|