@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
|
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, Trash2, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { isInsideLaterFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
|
|
5
|
+
import { DatePicker, } from "../datepicker/index.js";
|
|
5
6
|
import { MenuCheckboxItem, useMenuKeyboard } from "../menu/index.js";
|
|
6
7
|
import { Select } from "../select/index.js";
|
|
7
8
|
import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
|
|
@@ -21,11 +22,24 @@ const OP_LABELS = {
|
|
|
21
22
|
// review.
|
|
22
23
|
empty: "empty",
|
|
23
24
|
notempty: "not empty",
|
|
25
|
+
lessthan: "less than",
|
|
26
|
+
greaterthan: "greater than",
|
|
27
|
+
before: "before",
|
|
28
|
+
after: "after",
|
|
24
29
|
};
|
|
25
30
|
// #619: order and label set match Attio's own text-filter operator picker
|
|
26
31
|
// verbatim (measured live, per RecordsTable's own #578 port of the same
|
|
27
32
|
// set: contains · does not contain · starts with · ends with · is · is not ·
|
|
28
|
-
// empty · not empty).
|
|
33
|
+
// empty · not empty). #593: number and date columns get their OWN shorter
|
|
34
|
+
// sets instead — also measured live (`app.attio.com`, a Twitter follower
|
|
35
|
+
// count / Foundation date column, each filter added then discarded): number
|
|
36
|
+
// is "is / is not / less than / greater than / empty / not empty" (104.86px
|
|
37
|
+
// wide, 6 rows); date is "is / is not / before / after / empty / not empty"
|
|
38
|
+
// (91.16px, 6 rows) — both narrower than the text set's own 156.5px, which
|
|
39
|
+
// is why `.crisp-tabletoolbar-menu--op`'s old fixed 134px floor is gone (see
|
|
40
|
+
// tabletoolbar.recipe.ts): every one of these three sets is real, live-
|
|
41
|
+
// measured content-driven width, and a floor sized for one of them
|
|
42
|
+
// overshoots the other two.
|
|
29
43
|
const OP_ORDER = [
|
|
30
44
|
"contains",
|
|
31
45
|
"doesnotcontain",
|
|
@@ -36,15 +50,44 @@ const OP_ORDER = [
|
|
|
36
50
|
"empty",
|
|
37
51
|
"notempty",
|
|
38
52
|
];
|
|
53
|
+
const NUMBER_OP_ORDER = [
|
|
54
|
+
"is",
|
|
55
|
+
"isnot",
|
|
56
|
+
"lessthan",
|
|
57
|
+
"greaterthan",
|
|
58
|
+
"empty",
|
|
59
|
+
"notempty",
|
|
60
|
+
];
|
|
61
|
+
const DATE_OP_ORDER = [
|
|
62
|
+
"is",
|
|
63
|
+
"isnot",
|
|
64
|
+
"before",
|
|
65
|
+
"after",
|
|
66
|
+
"empty",
|
|
67
|
+
"notempty",
|
|
68
|
+
];
|
|
39
69
|
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
40
|
-
// nothing to check off otherwise.
|
|
41
|
-
|
|
70
|
+
// nothing to check off otherwise. A column's `type` picks the base set
|
|
71
|
+
// first; "isanyof" can still follow any of the three (a column's own
|
|
72
|
+
// declared values aren't tied to its underlying number/date/text kind).
|
|
73
|
+
const opsFor = (col) => {
|
|
74
|
+
const base = col?.type === "number"
|
|
75
|
+
? NUMBER_OP_ORDER
|
|
76
|
+
: col?.type === "date"
|
|
77
|
+
? DATE_OP_ORDER
|
|
78
|
+
: OP_ORDER;
|
|
79
|
+
return col?.values?.length ? [...base, "isanyof"] : base;
|
|
80
|
+
};
|
|
42
81
|
const opNeedsValue = (op) => op === "contains" ||
|
|
43
82
|
op === "doesnotcontain" ||
|
|
44
83
|
op === "startswith" ||
|
|
45
84
|
op === "endswith" ||
|
|
46
85
|
op === "is" ||
|
|
47
|
-
op === "isnot"
|
|
86
|
+
op === "isnot" ||
|
|
87
|
+
op === "lessthan" ||
|
|
88
|
+
op === "greaterthan" ||
|
|
89
|
+
op === "before" ||
|
|
90
|
+
op === "after";
|
|
48
91
|
const opNeedsValues = (op) => op === "isanyof";
|
|
49
92
|
// "Department is any of IT, Finance" past two values becomes "… +N": the
|
|
50
93
|
// chip is one line, and a field can have far more than two facet values.
|
|
@@ -59,9 +102,10 @@ const asText = (v) => {
|
|
|
59
102
|
return v.join(", ");
|
|
60
103
|
// Before the generic object branch, which would look for `.name`, find none
|
|
61
104
|
// and return "" — turning every Date into the empty string (#185). The ISO
|
|
62
|
-
// date is stable and locale-independent, so `contains "2026-06"` works
|
|
63
|
-
//
|
|
64
|
-
//
|
|
105
|
+
// date is stable and locale-independent, so `contains "2026-06"` works and
|
|
106
|
+
// (#593) so does `is`/`is not` against a `type: "date"` column's own ISO
|
|
107
|
+
// condition value — `before`/`after` get a real chronological compare
|
|
108
|
+
// instead, below.
|
|
65
109
|
if (v instanceof Date)
|
|
66
110
|
return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
|
|
67
111
|
if (typeof v === "object") {
|
|
@@ -70,13 +114,67 @@ const asText = (v) => {
|
|
|
70
114
|
}
|
|
71
115
|
return String(v);
|
|
72
116
|
};
|
|
117
|
+
// #593: `lessthan`/`greaterthan` (number columns) and `before`/`after` (date
|
|
118
|
+
// columns) need a real numeric/chronological compare, not `asText`'s string
|
|
119
|
+
// one — `null` on anything that doesn't parse, so a condition against a
|
|
120
|
+
// malformed or missing value never matches rather than throwing.
|
|
121
|
+
const toNumberOrNull = (v) => {
|
|
122
|
+
if (typeof v === "number")
|
|
123
|
+
return Number.isNaN(v) ? null : v;
|
|
124
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
125
|
+
const n = Number(v);
|
|
126
|
+
return Number.isNaN(n) ? null : n;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
const toDateOrNull = (v) => {
|
|
131
|
+
if (v instanceof Date)
|
|
132
|
+
return Number.isNaN(v.getTime()) ? null : v;
|
|
133
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
134
|
+
const d = new Date(v);
|
|
135
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
// A `type: "date"` condition's own value is stored as a plain ISO date
|
|
140
|
+
// string ("2026-09-25") — machine-comparable (`before`/`after` above,
|
|
141
|
+
// `is`/`is not` via `asText`) and locale-independent, the same choice #185
|
|
142
|
+
// already made for `asText`'s own `Date` branch. Parsed/formatted at local
|
|
143
|
+
// midnight, not UTC, so the picked calendar day and the displayed day are
|
|
144
|
+
// always the same one regardless of timezone offset.
|
|
145
|
+
const isoToDate = (iso) => {
|
|
146
|
+
if (!iso)
|
|
147
|
+
return null;
|
|
148
|
+
const d = new Date(`${iso}T00:00:00`);
|
|
149
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
150
|
+
};
|
|
151
|
+
const dateToISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
152
|
+
// e.g. "Sep 25, 2026" — the same format `DatePickerField`'s own default
|
|
153
|
+
// uses, and what Attio's chip itself showed after picking "Today" live.
|
|
154
|
+
const formatFilterDate = (iso) => {
|
|
155
|
+
const d = isoToDate(iso);
|
|
156
|
+
return d
|
|
157
|
+
? d.toLocaleDateString(undefined, {
|
|
158
|
+
month: "short",
|
|
159
|
+
day: "numeric",
|
|
160
|
+
year: "numeric",
|
|
161
|
+
})
|
|
162
|
+
: iso;
|
|
163
|
+
};
|
|
164
|
+
const toDatePickerDate = (d) => ({
|
|
165
|
+
year: d.getFullYear(),
|
|
166
|
+
month: d.getMonth() + 1,
|
|
167
|
+
day: d.getDate(),
|
|
168
|
+
});
|
|
169
|
+
const fromDatePickerDate = (d) => new Date(d.year, d.month - 1, d.day);
|
|
73
170
|
/**
|
|
74
171
|
* Applies a toolbar's filter conditions to any row array. Reads the raw value
|
|
75
172
|
* at `row[key]` (override with `get` for derived fields) — never a formatted
|
|
76
173
|
* display string, so a grouped-thousands "$1,200" still matches "1200".
|
|
77
174
|
*/
|
|
78
175
|
export function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
79
|
-
const active = filters.filter((f) =>
|
|
176
|
+
const active = filters.filter((f) => f.op != null &&
|
|
177
|
+
(!opNeedsValue(f.op) || f.value.trim() !== "") &&
|
|
80
178
|
(!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
|
|
81
179
|
if (!active.length)
|
|
82
180
|
return rows;
|
|
@@ -86,6 +184,20 @@ export function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
|
86
184
|
return isEmptyValue(raw);
|
|
87
185
|
if (f.op === "notempty")
|
|
88
186
|
return !isEmptyValue(raw);
|
|
187
|
+
if (f.op === "lessthan" || f.op === "greaterthan") {
|
|
188
|
+
const n = toNumberOrNull(raw);
|
|
189
|
+
const cn = toNumberOrNull(f.value);
|
|
190
|
+
if (n == null || cn == null)
|
|
191
|
+
return false;
|
|
192
|
+
return f.op === "lessthan" ? n < cn : n > cn;
|
|
193
|
+
}
|
|
194
|
+
if (f.op === "before" || f.op === "after") {
|
|
195
|
+
const d = toDateOrNull(raw);
|
|
196
|
+
const cd = toDateOrNull(f.value);
|
|
197
|
+
if (d == null || cd == null)
|
|
198
|
+
return false;
|
|
199
|
+
return f.op === "before" ? d < cd : d > cd;
|
|
200
|
+
}
|
|
89
201
|
const t = asText(raw).toLowerCase();
|
|
90
202
|
if (f.op === "isanyof")
|
|
91
203
|
return (f.values ?? []).some((v) => v.toLowerCase() === t);
|
|
@@ -144,7 +256,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
144
256
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
145
257
|
* Created by), which one shared list can't reproduce.
|
|
146
258
|
*/
|
|
147
|
-
export 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 }) {
|
|
259
|
+
export 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 }) {
|
|
148
260
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
149
261
|
// CrispMessagesProvider's translation of the English default below, or
|
|
150
262
|
// that default itself with no provider — unchanged from before this
|
|
@@ -278,11 +390,29 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
278
390
|
};
|
|
279
391
|
const addFilter = (key) => {
|
|
280
392
|
const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
|
|
281
|
-
putFilters([...filters, { id, key,
|
|
393
|
+
putFilters([...filters, { id, key, value: "" }]);
|
|
282
394
|
setFilterQuery("");
|
|
283
|
-
|
|
395
|
+
// matches Attio: picking a field opens the operator menu next, not a
|
|
396
|
+
// combined dialog — measured live, 2026-09-25.
|
|
397
|
+
setOpenMenu(`fop:${id}`);
|
|
284
398
|
};
|
|
285
399
|
const patchFilter = (id, patch) => putFilters(filters.map((c) => (c.id === id ? { ...c, ...patch } : c)));
|
|
400
|
+
// The field segment's own picker (#462 follow-up): changing the field
|
|
401
|
+
// replaces it in place — confirmed live, one chip, not a second — and
|
|
402
|
+
// resets op/value/values, since neither is guaranteed to make sense for
|
|
403
|
+
// the new field's type (a text op on a select field, e.g.).
|
|
404
|
+
const changeFilterField = (id, key) => {
|
|
405
|
+
patchFilter(id, { key, op: undefined, value: "", values: undefined });
|
|
406
|
+
setFilterQuery("");
|
|
407
|
+
setOpenMenu(`fop:${id}`);
|
|
408
|
+
};
|
|
409
|
+
// The operator segment's own menu: picking an operator that needs a value
|
|
410
|
+
// auto-opens the value popover, focused; "empty"/"not empty" need nothing
|
|
411
|
+
// further, so the menu just closes — both measured live, 2026-09-25.
|
|
412
|
+
const pickOp = (id, op) => {
|
|
413
|
+
patchFilter(id, { op });
|
|
414
|
+
setOpenMenu(opNeedsValue(op) || opNeedsValues(op) ? `fval:${id}` : null);
|
|
415
|
+
};
|
|
286
416
|
const removeFilter = (id) => {
|
|
287
417
|
putFilters(filters.filter((c) => c.id !== id));
|
|
288
418
|
setOpenMenu(null);
|
|
@@ -321,6 +451,23 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
321
451
|
close: () => setOpenMenu(null),
|
|
322
452
|
contentRef: kebabMenuRef,
|
|
323
453
|
});
|
|
454
|
+
// The operator popover is `role="menu"` too — confirmed live, 2026-09-25 —
|
|
455
|
+
// so it gets the same shared keyboard model as the kebab above, one hook
|
|
456
|
+
// call tracking whichever condition's operator menu is open.
|
|
457
|
+
const opMenuRef = React.useRef(null);
|
|
458
|
+
const { onKeyDown: onOpKeyDown } = useMenuKeyboard({
|
|
459
|
+
open: typeof openMenu === "string" && openMenu.startsWith("fop:"),
|
|
460
|
+
close: () => setOpenMenu(null),
|
|
461
|
+
contentRef: opMenuRef,
|
|
462
|
+
});
|
|
463
|
+
// Shared between the "Filter" trigger's own add-condition picker and each
|
|
464
|
+
// chip's field segment (#462 follow-up) — both search the same
|
|
465
|
+
// `filterColumns` list and differ only in what a pick does.
|
|
466
|
+
const filterFieldPicker = (onSelect) => (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
|
|
467
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
468
|
+
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
|
|
469
|
+
.filter((col) => col.label.toLowerCase().includes(filterQuery.trim().toLowerCase()))
|
|
470
|
+
.map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => onSelect(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] }));
|
|
324
471
|
const activeFilters = filters.length;
|
|
325
472
|
// The default phrase only works in English word order, so a caller can
|
|
326
473
|
// replace the whole thing rather than fill a template — crisp/#471's
|
|
@@ -335,7 +482,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
335
482
|
: sorts.length
|
|
336
483
|
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
337
484
|
: sortLabel;
|
|
338
|
-
return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
485
|
+
return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
339
486
|
if (dragSort !== null)
|
|
340
487
|
e.preventDefault();
|
|
341
488
|
}, onDrop: () => {
|
|
@@ -357,47 +504,83 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
357
504
|
label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
|
|
358
505
|
keywords: descendingLabel,
|
|
359
506
|
},
|
|
360
|
-
], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), _jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
507
|
+
], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
361
508
|
? filterTriggerLabel(activeFilters)
|
|
362
509
|
: activeFilters
|
|
363
510
|
? `${filterLabel} · ${activeFilters}`
|
|
364
|
-
: filterLabel] }), openMenu === "filter" && (
|
|
365
|
-
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
366
|
-
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
|
|
367
|
-
.filter((col) => col.label
|
|
368
|
-
.toLowerCase()
|
|
369
|
-
.includes(filterQuery.trim().toLowerCase()))
|
|
370
|
-
.map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
|
|
511
|
+
: filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
|
|
371
512
|
const col = filterColumns.find((c) => c.key === cond.key);
|
|
372
|
-
const
|
|
513
|
+
const fieldOpen = openMenu === `ffield:${cond.id}`;
|
|
514
|
+
const opOpen = openMenu === `fop:${cond.id}`;
|
|
515
|
+
const valOpen = openMenu === `fval:${cond.id}`;
|
|
373
516
|
const kebabOpen = openMenu === `fkebab:${cond.id}`;
|
|
374
|
-
|
|
517
|
+
// Gates the field/value segments' background (white once the
|
|
518
|
+
// condition is usable, the measured `f6f7f7` otherwise) — NOT
|
|
519
|
+
// whether the value popover renders; see `showValueSeg` below.
|
|
520
|
+
const complete = cond.op != null &&
|
|
521
|
+
(!opNeedsValue(cond.op) || cond.value.trim()) &&
|
|
375
522
|
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
523
|
+
// No value segment at all for "empty"/"not empty" (nothing to
|
|
524
|
+
// show) or before an operator is even chosen — matches Attio,
|
|
525
|
+
// which never renders a third box in either case.
|
|
526
|
+
const showValueSeg = cond.op != null &&
|
|
527
|
+
(opNeedsValue(cond.op) || opNeedsValues(cond.op));
|
|
528
|
+
return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [_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 ? _jsx(col.icon, { size: 14, strokeWidth: 1.75 }) : null, col?.label] }), _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
|
|
529
|
+
? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
|
|
530
|
+
: selectConditionLabel }), showValueSeg && (_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"
|
|
531
|
+
? cond.values?.length
|
|
532
|
+
? isAnyOfSummary(cond.values)
|
|
533
|
+
: filterValuePlaceholder
|
|
534
|
+
: cond.value.trim()
|
|
535
|
+
? col?.type === "date"
|
|
536
|
+
? formatFilterDate(cond.value)
|
|
537
|
+
: cond.value
|
|
538
|
+
: filterValuePlaceholder })), _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: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_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: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && (_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) => (_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 ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && (_jsx("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
539
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
540
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
541
|
+
.filter((v) => v
|
|
542
|
+
.toLowerCase()
|
|
543
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
544
|
+
.map((v) => {
|
|
545
|
+
const checked = (cond.values ?? []).includes(v);
|
|
546
|
+
return (_jsx(MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
|
|
547
|
+
const set = new Set(cond.values ?? []);
|
|
548
|
+
if (next)
|
|
549
|
+
set.add(v);
|
|
550
|
+
else
|
|
551
|
+
set.delete(v);
|
|
552
|
+
// the column's own order, not click order —
|
|
553
|
+
// what the chip summary reads out
|
|
554
|
+
patchFilter(cond.id, {
|
|
555
|
+
values: col.values?.filter((x) => set.has(x)),
|
|
556
|
+
});
|
|
557
|
+
}, children: v }, v));
|
|
558
|
+
})] })) : col?.type === "date" ? (_jsxs("div", { className: "crisp-tabletoolbar-datepop", children: [_jsx(DatePicker, { ...{
|
|
559
|
+
selectionMode: "single",
|
|
560
|
+
value: isoToDate(cond.value)
|
|
561
|
+
? toDatePickerDate(isoToDate(cond.value))
|
|
562
|
+
: undefined,
|
|
563
|
+
onValueChange: (next) => {
|
|
564
|
+
if (!next)
|
|
565
|
+
return;
|
|
566
|
+
patchFilter(cond.id, {
|
|
567
|
+
value: dateToISO(fromDatePickerDate(next)),
|
|
568
|
+
});
|
|
569
|
+
setOpenMenu(null);
|
|
570
|
+
},
|
|
571
|
+
} }), _jsx("div", { className: "crisp-tabletoolbar-datepop-presets", children: [
|
|
572
|
+
[crispMessages.Today ?? "Today", 0],
|
|
573
|
+
[crispMessages.Tomorrow ?? "Tomorrow", 1],
|
|
574
|
+
[crispMessages["Next week"] ?? "Next week", 7],
|
|
575
|
+
].map(([label, addDays]) => (_jsx("button", { type: "button", className: "crisp-tabletoolbar-datepop-preset", onClick: () => {
|
|
576
|
+
const d = new Date();
|
|
577
|
+
d.setDate(d.getDate() + addDays);
|
|
578
|
+
patchFilter(cond.id, { value: dateToISO(d) });
|
|
579
|
+
setOpenMenu(null);
|
|
580
|
+
}, children: label }, label))) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
581
|
+
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
582
|
+
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
|
|
583
|
+
}), showViewSettings && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
|
|
401
584
|
if (openMenu === "viewsettings") {
|
|
402
585
|
setOpenMenu(null);
|
|
403
586
|
}
|
|
@@ -421,7 +604,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
421
604
|
moveAttr(dragAttr, i);
|
|
422
605
|
setDragAttr(null);
|
|
423
606
|
}, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
|
|
424
|
-
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
607
|
+
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
425
608
|
}
|
|
426
609
|
/**
|
|
427
610
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
2
|
import { contract as c } from "../../tokens/index.js";
|
|
3
3
|
import { toolbarChromeBody, toolbarChromeHead } from "./tabletoolbar.chrome.js";
|
|
4
|
+
// The filter condition chip, TableToolbar-only (crisp/#462 follow-up,
|
|
5
|
+
// re-measured live 2026-09-25). Deliberately NOT added to the shared
|
|
6
|
+
// `tabletoolbar.chrome.ts` functions: those also render under RecordsTable's
|
|
7
|
+
// own `crisp-recordstable-` prefix, whose still-tested (84 cases) chip
|
|
8
|
+
// predates this re-measurement and keeps its original `.fchip`/`.fchip-main`
|
|
9
|
+
// markup (one combined field+op+value button, plus a small floating kebab)
|
|
10
|
+
// until RecordsTable is rebuilt onto DataTable + TableToolbar and inherits
|
|
11
|
+
// this one instead. New, disjoint class names here so nothing shared changes
|
|
12
|
+
// meaning under the older markup.
|
|
13
|
+
//
|
|
14
|
+
// Measured on Attio's own chip (`app.attio.com/<ws>/companies`, a text-field
|
|
15
|
+
// condition): 4 independently-clickable, edge-to-edge segments, each 28px
|
|
16
|
+
// tall with its own `inset 0 0 0 1px` ring — field (radius 8/0/0/8), operator
|
|
17
|
+
// (flat), value (flat), kebab (radius 0/8/8/0). Background is NOT fixed per
|
|
18
|
+
// segment: the field and value segments (the two that show the condition's
|
|
19
|
+
// own text) read `rgb(246,247,247)` while the condition has no value yet and
|
|
20
|
+
// flip to white the instant it does (typing a value, or picking an operator
|
|
21
|
+
// like "empty" that needs none, both trigger it); the operator and kebab
|
|
22
|
+
// segments (pure controls, no text of the condition's own) stay white
|
|
23
|
+
// throughout. Click targets, confirmed live: the field segment reopens the
|
|
24
|
+
// attribute picker and replaces this condition's field in place (not a
|
|
25
|
+
// second chip); the operator segment opens an operator-only menu (closing it
|
|
26
|
+
// after a pick, then auto-opening the value popover, focused, when that
|
|
27
|
+
// operator needs one — "empty"/"not empty" just close); the value segment
|
|
28
|
+
// opens a value-only popover/input, independent of the operator menu (Attio
|
|
29
|
+
// never combines the two, unlike crisp's older single dialog).
|
|
30
|
+
const filterChipChrome = `
|
|
31
|
+
& .crisp-tabletoolbar-fcond { display: inline-flex; align-items: center; height: ${c.size.row}; font-size: ${c.text.md}; color: ${c.fgPrimary}; white-space: nowrap; }
|
|
32
|
+
& .crisp-tabletoolbar-fcond-seg { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 100%; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: ${c.bgSurfaceMuted}; box-shadow: inset 0 0 0 1px ${c.borderDivider}; font-family: inherit; font-size: inherit; color: inherit; cursor: pointer; }
|
|
33
|
+
& .crisp-tabletoolbar-fcond-seg svg { flex-shrink: 0; }
|
|
34
|
+
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--field,
|
|
35
|
+
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: ${c.bgSurface}; }
|
|
36
|
+
& .crisp-tabletoolbar-fcond-seg--field { border-radius: ${c.radius.md} 0 0 ${c.radius.md}; font-weight: ${c.weight.medium}; }
|
|
37
|
+
& .crisp-tabletoolbar-fcond-seg--field svg { color: ${c.fgMuted}; }
|
|
38
|
+
& .crisp-tabletoolbar-fcond-seg--op { background: ${c.bgSurface}; color: ${c.fgMuted}; }
|
|
39
|
+
& .crisp-tabletoolbar-fcond-seg--val { font-weight: ${c.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
|
|
40
|
+
& .crisp-tabletoolbar-fcond-seg--kebab { background: ${c.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; color: ${c.fgMuted}; }
|
|
41
|
+
& .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${c.fgPrimary}; }
|
|
42
|
+
& .crisp-tabletoolbar-menu--val { min-width: 134px; }
|
|
43
|
+
& .crisp-tabletoolbar-datepop { width: 300px; }
|
|
44
|
+
& .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${c.space["1"]}; margin-top: ${c.space["2"]}; padding: 0 ${c.space["1"]}; }
|
|
45
|
+
& .crisp-tabletoolbar-datepop-preset { height: ${c.size.chip}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.chip}; background: ${c.bgRaised}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.sm}; cursor: pointer; }
|
|
46
|
+
& .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); }`;
|
|
4
47
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
5
48
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
6
49
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -17,6 +60,7 @@ const config = defineRecipe({
|
|
|
17
60
|
& * { box-sizing: border-box; }
|
|
18
61
|
${toolbarChromeHead("tabletoolbar")}
|
|
19
62
|
${toolbarChromeBody("tabletoolbar")}
|
|
63
|
+
${filterChipChrome}
|
|
20
64
|
`,
|
|
21
65
|
});
|
|
22
66
|
export const tabletoolbarRecipe = resolveRecipe(config);
|
|
@@ -11,7 +11,16 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
11
11
|
// inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
|
|
12
12
|
// a 16px leading icon and an optional count badge. Measured from the live
|
|
13
13
|
// app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
|
|
14
|
-
// gap, 14/500 labels
|
|
14
|
+
// gap, 14/500 labels. The count badge (crisp#697, re-measured against a
|
|
15
|
+
// record's tab counts and a settings "Attributes 35" count) is 14px
|
|
16
|
+
// tall/wide-min, radius 4px (no scale token lands there -- a recipe-local
|
|
17
|
+
// literal, same precedent as this file's own icon/gap literals), 10/500/14
|
|
18
|
+
// text on a 4% `fgPrimary` tint (`rgba(0,0,0,.04)`) with a measured
|
|
19
|
+
// `inset 0 0 0 1px borderSubtle` ring, text `fgNeutralSoft` (rgb(80,81,85),
|
|
20
|
+
// the same token Badge's neutral tone uses, #696) -- was 16px/r6/11px/
|
|
21
|
+
// `fgMuted`/6% tint with no ring at all. The selected tab's own badge
|
|
22
|
+
// colour (`fgPrimary`) wasn't measured (the live Overview tab carries no
|
|
23
|
+
// count) and stays as-is.
|
|
15
24
|
//
|
|
16
25
|
// The panel's own `padding-top` (`c.space["8"]`, 32px) is measured
|
|
17
26
|
// separately, live, on a company record page (Overview tab): the gap from
|
|
@@ -42,7 +51,7 @@ const config = defineRecipe({
|
|
|
42
51
|
& .crisp-tabs-tab:hover:not([data-selected]) { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
|
|
43
52
|
& .crisp-tabs-tab[data-selected] { color: ${c.fgPrimary}; background: ${c.bgSubtle}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; }
|
|
44
53
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
|
|
45
|
-
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width:
|
|
54
|
+
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${c.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
|
|
46
55
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${c.fgPrimary}; }
|
|
47
56
|
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${c.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; }
|
|
48
57
|
& .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${c.space["8"]}; }
|
|
@@ -67,12 +67,24 @@ style: _style, ...rest }, forwardedRef) => {
|
|
|
67
67
|
if (!el)
|
|
68
68
|
return;
|
|
69
69
|
const r = el.getBoundingClientRect();
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
const placement = side === "right"
|
|
71
|
+
? "right"
|
|
72
|
+
: side === "bottom" || r.top < 48
|
|
73
|
+
? "bottom"
|
|
74
|
+
: "top";
|
|
75
|
+
setPos(placement === "right"
|
|
76
|
+
? {
|
|
77
|
+
// Measured (crisp#633): the bubble's left edge sits 8px past
|
|
78
|
+
// the trigger's right edge, vertically centred on it.
|
|
79
|
+
left: Math.round(r.right + 8),
|
|
80
|
+
top: Math.round(r.top + r.height / 2),
|
|
81
|
+
placement,
|
|
82
|
+
}
|
|
83
|
+
: {
|
|
84
|
+
left: Math.round(r.left + r.width / 2),
|
|
85
|
+
top: Math.round(placement === "bottom" ? r.bottom + 6 : r.top - 6),
|
|
86
|
+
placement,
|
|
87
|
+
});
|
|
76
88
|
}, [open, side]);
|
|
77
89
|
const openAfterDelay = () => {
|
|
78
90
|
clearTimeout(timer.current);
|
|
@@ -119,9 +131,11 @@ style: _style, ...rest }, forwardedRef) => {
|
|
|
119
131
|
createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe({ variant }), style: {
|
|
120
132
|
left: pos.left,
|
|
121
133
|
top: pos.top,
|
|
122
|
-
transform: pos.
|
|
123
|
-
? "translate(-50
|
|
124
|
-
:
|
|
134
|
+
transform: pos.placement === "right"
|
|
135
|
+
? "translate(0, -50%)"
|
|
136
|
+
: pos.placement === "bottom"
|
|
137
|
+
? "translate(-50%, 0)"
|
|
138
|
+
: "translate(-50%, -100%)",
|
|
125
139
|
}, children: content }), document.body);
|
|
126
140
|
if (asChild) {
|
|
127
141
|
// No wrapper at all: the child carries the hover/focus/Escape
|
|
@@ -13,14 +13,15 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
13
13
|
// just the wrong pair.
|
|
14
14
|
//
|
|
15
15
|
// crisp#563: this `boxShadow` is Attio's PLAIN tooltip (a truncated header
|
|
16
|
-
// label, measured on "Twitter follower count")
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
16
|
+
// label, measured on "Twitter follower count"). Re-measured live and
|
|
17
|
+
// corroborated against a second sample (hovering the same field a second
|
|
18
|
+
// time) per AUTHORING.md Lesson 22 — the value below is an inset ring plus
|
|
19
|
+
// two offset shadows, not the single offset shadow this recipe carried
|
|
20
|
+
// before. Attio has a SECOND, visually similar but structurally different
|
|
21
|
+
// tooltip for a reference-column header (icon + the referenced type's name
|
|
22
|
+
// after the label, crisp#580) with its own measured shadow, `variant:
|
|
23
|
+
// "reference"` below. Background, text colour, radius, padding and font are
|
|
24
|
+
// identical between the two measured samples; only the shadow differs.
|
|
24
25
|
const spec = defineSpec({
|
|
25
26
|
name: "tooltip",
|
|
26
27
|
base: {
|
|
@@ -37,7 +38,7 @@ const spec = defineSpec({
|
|
|
37
38
|
fontSize: c.text.sm,
|
|
38
39
|
fontWeight: c.weight.medium,
|
|
39
40
|
lineHeight: "1.45",
|
|
40
|
-
boxShadow: "0
|
|
41
|
+
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",
|
|
41
42
|
opacity: 1,
|
|
42
43
|
transition: "opacity 0.12s ease",
|
|
43
44
|
},
|