@crisp-ui-kit/crisp 0.54.3 → 0.54.4
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/board.css +13 -1
- package/dist/board.layered.css +13 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +41 -4
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/product/board/board.js +60 -2
- package/dist/cjs/product/board/board.recipe.js +10 -0
- package/dist/cjs/product/comments/comments.js +426 -0
- package/dist/cjs/product/comments/comments.recipe.js +126 -0
- package/dist/cjs/product/comments/index.js +8 -0
- package/dist/cjs/product/listpage/listpage.recipe.js +10 -1
- package/dist/cjs/product/objectsettings/index.js +16 -0
- package/dist/cjs/product/objectsettings/objectsettings.js +365 -0
- package/dist/cjs/product/objectsettings/objectsettings.recipe.js +65 -0
- package/dist/cjs/product/taskstable/taskstable.recipe.js +10 -1
- package/dist/comments.css +80 -0
- package/dist/comments.d.ts +7 -0
- package/dist/comments.layered.css +82 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +43 -6
- package/dist/esm/index.js +2 -0
- package/dist/esm/product/board/board.js +61 -3
- package/dist/esm/product/board/board.recipe.js +10 -0
- package/dist/esm/product/comments/comments.js +388 -0
- package/dist/esm/product/comments/comments.recipe.js +123 -0
- package/dist/esm/product/comments/index.js +2 -0
- package/dist/esm/product/listpage/listpage.recipe.js +10 -1
- package/dist/esm/product/objectsettings/index.js +2 -0
- package/dist/esm/product/objectsettings/objectsettings.js +322 -0
- package/dist/esm/product/objectsettings/objectsettings.recipe.js +62 -0
- package/dist/esm/product/taskstable/taskstable.recipe.js +10 -1
- package/dist/listpage.css +8 -1
- package/dist/listpage.layered.css +8 -1
- package/dist/objectsettings.css +49 -0
- package/dist/objectsettings.d.ts +7 -0
- package/dist/objectsettings.layered.css +51 -0
- package/dist/styles.css +160 -3
- package/dist/styles.layered.css +160 -3
- package/dist/taskstable.css +8 -1
- package/dist/taskstable.layered.css +8 -1
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +8 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/product/board/board.d.ts +20 -1
- package/dist/types/product/comments/comments.d.ts +192 -0
- package/dist/types/product/comments/comments.recipe.d.ts +3 -0
- package/dist/types/product/comments/index.d.ts +2 -0
- package/dist/types/product/objectsettings/index.d.ts +2 -0
- package/dist/types/product/objectsettings/objectsettings.d.ts +261 -0
- package/dist/types/product/objectsettings/objectsettings.recipe.d.ts +3 -0
- package/package.json +11 -1
package/dist/board.css
CHANGED
|
@@ -16,8 +16,20 @@
|
|
|
16
16
|
& .crisp-board-card[data-dragging] { opacity: 0.4; }
|
|
17
17
|
& .crisp-board-card:focus { outline: none; }
|
|
18
18
|
& .crisp-board-card:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
|
|
19
|
+
/* #604: a per-column calc footer, mirroring the table's own (measured
|
|
20
|
+
live, Deals pipeline). Sits below the scrollable card body, not
|
|
21
|
+
inside it or pinned to it -- Attio's own is position: static, not sticky, so it's a plain flex-shrink:0 sibling here too. */
|
|
22
|
+
& .crisp-board-col-footer { flex-shrink: 0; padding: var(--crisp-space-1) 0; }
|
|
23
|
+
& .crisp-board-col-calc { display: flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); width: 100%; height: 28px; padding: var(--crisp-space-1) var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); cursor: pointer; }
|
|
24
|
+
& .crisp-board-col-calc:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
|
|
25
|
+
& .crisp-board-col-calc[data-empty] { opacity: 0.7; }
|
|
26
|
+
& .crisp-board-col-calc-label { text-transform: capitalize; }
|
|
27
|
+
& .crisp-board-col-calc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
|
|
19
28
|
/* announced, never drawn */
|
|
20
|
-
& .crisp-board-live {
|
|
29
|
+
& .crisp-board-live {
|
|
30
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
31
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
32
|
+
}
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
@media (prefers-reduced-motion: reduce) {
|
package/dist/board.layered.css
CHANGED
|
@@ -17,8 +17,20 @@
|
|
|
17
17
|
& .crisp-board-card[data-dragging] { opacity: 0.4; }
|
|
18
18
|
& .crisp-board-card:focus { outline: none; }
|
|
19
19
|
& .crisp-board-card:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
|
|
20
|
+
/* #604: a per-column calc footer, mirroring the table's own (measured
|
|
21
|
+
live, Deals pipeline). Sits below the scrollable card body, not
|
|
22
|
+
inside it or pinned to it -- Attio's own is position: static, not sticky, so it's a plain flex-shrink:0 sibling here too. */
|
|
23
|
+
& .crisp-board-col-footer { flex-shrink: 0; padding: var(--crisp-space-1) 0; }
|
|
24
|
+
& .crisp-board-col-calc { display: flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); width: 100%; height: 28px; padding: var(--crisp-space-1) var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); cursor: pointer; }
|
|
25
|
+
& .crisp-board-col-calc:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
|
|
26
|
+
& .crisp-board-col-calc[data-empty] { opacity: 0.7; }
|
|
27
|
+
& .crisp-board-col-calc-label { text-transform: capitalize; }
|
|
28
|
+
& .crisp-board-col-calc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
|
|
20
29
|
/* announced, never drawn */
|
|
21
|
-
& .crisp-board-live {
|
|
30
|
+
& .crisp-board-live {
|
|
31
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
32
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
33
|
+
}
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -46,14 +46,24 @@ const select_1 = require("../select");
|
|
|
46
46
|
const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
|
|
47
47
|
const OP_LABELS = {
|
|
48
48
|
contains: "contains",
|
|
49
|
+
doesnotcontain: "does not contain",
|
|
50
|
+
startswith: "starts with",
|
|
51
|
+
endswith: "ends with",
|
|
49
52
|
is: "is",
|
|
50
53
|
isnot: "is not",
|
|
51
54
|
isanyof: "is any of",
|
|
52
55
|
empty: "is empty",
|
|
53
56
|
notempty: "is not empty",
|
|
54
57
|
};
|
|
58
|
+
// #619: order and label set match Attio's own text-filter operator picker
|
|
59
|
+
// verbatim (measured live, per RecordsTable's own #578 port of the same
|
|
60
|
+
// set: contains · does not contain · starts with · ends with · is · is not ·
|
|
61
|
+
// empty · not empty).
|
|
55
62
|
const OP_ORDER = [
|
|
56
63
|
"contains",
|
|
64
|
+
"doesnotcontain",
|
|
65
|
+
"startswith",
|
|
66
|
+
"endswith",
|
|
57
67
|
"is",
|
|
58
68
|
"isnot",
|
|
59
69
|
"empty",
|
|
@@ -62,7 +72,12 @@ const OP_ORDER = [
|
|
|
62
72
|
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
63
73
|
// nothing to check off otherwise.
|
|
64
74
|
const opsFor = (col) => col?.values?.length ? [...OP_ORDER, "isanyof"] : OP_ORDER;
|
|
65
|
-
const opNeedsValue = (op) => op === "contains" ||
|
|
75
|
+
const opNeedsValue = (op) => op === "contains" ||
|
|
76
|
+
op === "doesnotcontain" ||
|
|
77
|
+
op === "startswith" ||
|
|
78
|
+
op === "endswith" ||
|
|
79
|
+
op === "is" ||
|
|
80
|
+
op === "isnot";
|
|
66
81
|
const opNeedsValues = (op) => op === "isanyof";
|
|
67
82
|
// "Department is any of IT, Finance" past two values becomes "… +N": the
|
|
68
83
|
// chip is one line, and a field can have far more than two facet values.
|
|
@@ -110,9 +125,15 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
|
110
125
|
const v = f.value.trim().toLowerCase();
|
|
111
126
|
if (f.op === "contains")
|
|
112
127
|
return t.includes(v);
|
|
128
|
+
if (f.op === "doesnotcontain")
|
|
129
|
+
return !t.includes(v);
|
|
130
|
+
if (f.op === "startswith")
|
|
131
|
+
return t.startsWith(v);
|
|
132
|
+
if (f.op === "endswith")
|
|
133
|
+
return t.endsWith(v);
|
|
113
134
|
if (f.op === "is")
|
|
114
135
|
return t === v;
|
|
115
|
-
return t !== v;
|
|
136
|
+
return t !== v; // isnot
|
|
116
137
|
}));
|
|
117
138
|
}
|
|
118
139
|
/**
|
|
@@ -156,7 +177,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
156
177
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
157
178
|
* Created by), which one shared list can't reproduce.
|
|
158
179
|
*/
|
|
159
|
-
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, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
|
|
180
|
+
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
|
|
160
181
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
161
182
|
// CrispMessagesProvider's translation of the English default below, or
|
|
162
183
|
// that default itself with no provider — unchanged from before this
|
|
@@ -175,6 +196,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
175
196
|
const filterFieldLabel = (0, core_1.useCrispMessage)(filterFieldLabelProp, "Filter by field");
|
|
176
197
|
const filterFieldPlaceholder = (0, core_1.useCrispMessage)(filterFieldPlaceholderProp, "Filter by…");
|
|
177
198
|
const filterValuePlaceholder = (0, core_1.useCrispMessage)(filterValuePlaceholderProp, "Value…");
|
|
199
|
+
const filterOptionsLabel = (0, core_1.useCrispMessage)(filterOptionsLabelProp, "Filter options");
|
|
200
|
+
const deleteFilterLabel = (0, core_1.useCrispMessage)(deleteFilterLabelProp, "Delete filter");
|
|
178
201
|
const ascendingLabel = (0, core_1.useCrispMessage)(ascendingLabelProp, "Ascending");
|
|
179
202
|
const descendingLabel = (0, core_1.useCrispMessage)(descendingLabelProp, "Descending");
|
|
180
203
|
// OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
|
|
@@ -269,6 +292,19 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
269
292
|
putFilters(filters.filter((c) => c.id !== id));
|
|
270
293
|
setOpenMenu(null);
|
|
271
294
|
};
|
|
295
|
+
// #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
|
|
296
|
+
// model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
|
|
297
|
+
// the sort/condition popovers above (those are role="dialog", which makes
|
|
298
|
+
// no such promise). `openMenu` already only ever names ONE open surface at
|
|
299
|
+
// a time, so one hook call at the top level — not one per filter row,
|
|
300
|
+
// which would break the Rules of Hooks inside `filters.map` below — tracks
|
|
301
|
+
// whichever chip's kebab is currently open via its content ref.
|
|
302
|
+
const kebabMenuRef = React.useRef(null);
|
|
303
|
+
const { onKeyDown: onKebabKeyDown } = (0, menu_1.useMenuKeyboard)({
|
|
304
|
+
open: typeof openMenu === "string" && openMenu.startsWith("fkebab:"),
|
|
305
|
+
close: () => setOpenMenu(null),
|
|
306
|
+
contentRef: kebabMenuRef,
|
|
307
|
+
});
|
|
272
308
|
const activeFilters = filters.length;
|
|
273
309
|
// The default phrase only works in English word order, so a caller can
|
|
274
310
|
// replace the whole thing rather than fill a template — crisp/#471's
|
|
@@ -318,10 +354,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
318
354
|
.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) => {
|
|
319
355
|
const col = filterColumns.find((c) => c.key === cond.key);
|
|
320
356
|
const open = openMenu === `fcond:${cond.id}`;
|
|
357
|
+
const kebabOpen = openMenu === `fkebab:${cond.id}`;
|
|
321
358
|
const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
|
|
322
359
|
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
323
360
|
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: crispMessages[OP_LABELS[cond.op]] ??
|
|
324
|
-
OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-
|
|
361
|
+
OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "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.jsx)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => removeFilter(cond.id), children: deleteFilterLabel }) })), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(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))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
325
362
|
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
326
363
|
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), 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`,
|
|
327
364
|
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
package/dist/cjs/index.js
CHANGED
|
@@ -89,6 +89,7 @@ __exportStar(require("./product/appshell"), exports);
|
|
|
89
89
|
__exportStar(require("./product/authshell"), exports);
|
|
90
90
|
__exportStar(require("./product/board"), exports);
|
|
91
91
|
__exportStar(require("./product/command"), exports);
|
|
92
|
+
__exportStar(require("./product/comments"), exports);
|
|
92
93
|
__exportStar(require("./product/entitycard"), exports);
|
|
93
94
|
__exportStar(require("./product/flowcanvas"), exports);
|
|
94
95
|
__exportStar(require("./product/formdialog"), exports);
|
|
@@ -97,6 +98,7 @@ __exportStar(require("./product/importdialog"), exports);
|
|
|
97
98
|
__exportStar(require("./product/inbox"), exports);
|
|
98
99
|
__exportStar(require("./product/listpage"), exports);
|
|
99
100
|
__exportStar(require("./product/navitem"), exports);
|
|
101
|
+
__exportStar(require("./product/objectsettings"), exports);
|
|
100
102
|
__exportStar(require("./product/onboarding"), exports);
|
|
101
103
|
__exportStar(require("./product/recordpage"), exports);
|
|
102
104
|
__exportStar(require("./product/recordpanel"), exports);
|
|
@@ -35,20 +35,76 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.Board = Board;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
|
+
const lucide_react_1 = require("lucide-react");
|
|
38
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const menu_1 = require("../../components/menu");
|
|
39
41
|
const core_1 = require("../../core");
|
|
40
42
|
const board_recipe_1 = require("./board.recipe");
|
|
43
|
+
const BOARD_AGG_ORDER = [
|
|
44
|
+
"countEmpty",
|
|
45
|
+
"countFilled",
|
|
46
|
+
"percentEmpty",
|
|
47
|
+
"percentFilled",
|
|
48
|
+
"sum",
|
|
49
|
+
"average",
|
|
50
|
+
"max",
|
|
51
|
+
"min",
|
|
52
|
+
];
|
|
53
|
+
// English default text, looked up through `useCrispMessages()` (crisp/#471)
|
|
54
|
+
// the same way Board's other built-in fragments already are.
|
|
55
|
+
const AGG_LABEL = {
|
|
56
|
+
countEmpty: "Count empty",
|
|
57
|
+
countFilled: "Count filled",
|
|
58
|
+
percentEmpty: "Percent empty",
|
|
59
|
+
percentFilled: "Percent filled",
|
|
60
|
+
sum: "Sum",
|
|
61
|
+
average: "Average",
|
|
62
|
+
max: "Max",
|
|
63
|
+
min: "Min",
|
|
64
|
+
};
|
|
65
|
+
function computeBoardAgg(agg, values) {
|
|
66
|
+
const filled = values.filter((v) => v != null);
|
|
67
|
+
const total = values.length;
|
|
68
|
+
switch (agg) {
|
|
69
|
+
case "countEmpty":
|
|
70
|
+
return String(total - filled.length);
|
|
71
|
+
case "countFilled":
|
|
72
|
+
return String(filled.length);
|
|
73
|
+
case "percentEmpty":
|
|
74
|
+
return total === 0
|
|
75
|
+
? "0%"
|
|
76
|
+
: `${Math.round(((total - filled.length) / total) * 100)}%`;
|
|
77
|
+
case "percentFilled":
|
|
78
|
+
return total === 0
|
|
79
|
+
? "0%"
|
|
80
|
+
: `${Math.round((filled.length / total) * 100)}%`;
|
|
81
|
+
case "sum":
|
|
82
|
+
return filled.reduce((a, b) => a + b, 0).toLocaleString();
|
|
83
|
+
case "average":
|
|
84
|
+
return filled.length === 0
|
|
85
|
+
? "0"
|
|
86
|
+
: (filled.reduce((a, b) => a + b, 0) / filled.length).toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
87
|
+
case "max":
|
|
88
|
+
return filled.length === 0 ? "—" : Math.max(...filled).toLocaleString();
|
|
89
|
+
case "min":
|
|
90
|
+
return filled.length === 0 ? "—" : Math.min(...filled).toLocaleString();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
41
93
|
/**
|
|
42
94
|
* A kanban board with native drag-and-drop between columns. Cards are
|
|
43
95
|
* `draggable`; dropping one on a column reparents it via `onCardsChange`
|
|
44
96
|
* (the parent owns the cards, so the move is a controlled state update).
|
|
45
97
|
* Measured from a deals pipeline.
|
|
46
98
|
*/
|
|
47
|
-
function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, className, ...rest }) {
|
|
99
|
+
function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, calc, className, ...rest }) {
|
|
48
100
|
// crisp/#471: same resolution order as every other component's own —
|
|
49
101
|
// explicit prop, then a CrispMessagesProvider's translation of the English
|
|
50
102
|
// default, then that default itself.
|
|
51
103
|
const newCardLabel = (0, core_1.useCrispMessage)(newCardLabelProp, "New");
|
|
104
|
+
// #604: which BoardAgg each column's footer shows, keyed by column id.
|
|
105
|
+
// Unset (undefined) means the column still shows the "+ Add calculation"
|
|
106
|
+
// trigger, exactly like the table footer's own per-column state.
|
|
107
|
+
const [colAgg, setColAgg] = React.useState({});
|
|
52
108
|
const crispMessages = (0, core_1.useCrispMessages)();
|
|
53
109
|
const announceMove = announceMoveProp ??
|
|
54
110
|
((column, card) => card
|
|
@@ -128,7 +184,9 @@ function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLa
|
|
|
128
184
|
// Ctrl+Right at the last column still reaches the browser
|
|
129
185
|
if (delta && moveByKey(card, delta))
|
|
130
186
|
e.preventDefault();
|
|
131
|
-
}, children: renderCard(card) }, card.id)))] })]
|
|
187
|
+
}, children: renderCard(card) }, card.id)))] }), calc && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-board-col-footer", children: (0, jsx_runtime_1.jsx)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-board-col-calc", "data-empty": colAgg[col.id] ? undefined : "", children: colAgg[col.id] ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-calc-label", children: crispMessages[AGG_LABEL[colAgg[col.id]]] ??
|
|
188
|
+
AGG_LABEL[colAgg[col.id]] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-board-col-calc-value", children: computeBoardAgg(colAgg[col.id], colCards.map((c) => calc.getValue(c))) })] })) : ((crispMessages["+ Add calculation"] ??
|
|
189
|
+
"+ Add calculation")) }), children: BOARD_AGG_ORDER.map((a) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => setColAgg((prev) => ({ ...prev, [col.id]: a })), children: [crispMessages[AGG_LABEL[a]] ?? AGG_LABEL[a], colAgg[col.id] === a ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 15, strokeWidth: 1.75 })) : null] }, a))) }) }))] }, col.id));
|
|
132
190
|
}), (0, jsx_runtime_1.jsx)("div", { className: "crisp-board-live", role: "status", "aria-live": "polite", children: announcement })] }));
|
|
133
191
|
}
|
|
134
192
|
Board.displayName = "Board";
|
|
@@ -35,6 +35,16 @@ const config = (0, core_1.defineRecipe)({
|
|
|
35
35
|
& .crisp-board-card[data-dragging] { opacity: 0.4; }
|
|
36
36
|
& .crisp-board-card:focus { outline: none; }
|
|
37
37
|
& .crisp-board-card:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
|
|
38
|
+
/* #604: a per-column calc footer, mirroring the table's own (measured
|
|
39
|
+
live, Deals pipeline). Sits below the scrollable card body, not
|
|
40
|
+
inside it or pinned to it -- Attio's own is position: static, not
|
|
41
|
+
sticky, so it's a plain flex-shrink:0 sibling here too. */
|
|
42
|
+
& .crisp-board-col-footer { flex-shrink: 0; padding: ${tokens_1.contract.space["1"]} 0; }
|
|
43
|
+
& .crisp-board-col-calc { display: flex; align-items: center; justify-content: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; height: 28px; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2"]}; margin: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); cursor: pointer; }
|
|
44
|
+
& .crisp-board-col-calc:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
|
|
45
|
+
& .crisp-board-col-calc[data-empty] { opacity: 0.7; }
|
|
46
|
+
& .crisp-board-col-calc-label { text-transform: capitalize; }
|
|
47
|
+
& .crisp-board-col-calc-value { color: ${tokens_1.contract.fgPrimary}; font-variant-numeric: tabular-nums; }
|
|
38
48
|
/* announced, never drawn */
|
|
39
49
|
& .crisp-board-live {
|
|
40
50
|
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|