@crisp-ui-kit/crisp 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/cjs/components/columncalc/columncalc.js +2 -2
  2. package/dist/cjs/components/multiselect/multiselect.js +2 -2
  3. package/dist/cjs/components/select/select.js +2 -2
  4. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +4 -4
  5. package/dist/cjs/components/wheelpicker/wheelpicker.js +8 -2
  6. package/dist/cjs/core/index.js +3 -1
  7. package/dist/cjs/core/use-prefers-reduced-motion.js +67 -0
  8. package/dist/cjs/product/recordpage/recordpage.js +8 -3
  9. package/dist/cjs/product/recordstable/recordstable.js +6 -3
  10. package/dist/cjs/product/sidebar/sidebar.js +2 -2
  11. package/dist/esm/components/columncalc/columncalc.js +2 -2
  12. package/dist/esm/components/multiselect/multiselect.js +2 -2
  13. package/dist/esm/components/select/select.js +2 -2
  14. package/dist/esm/components/tabletoolbar/tabletoolbar.js +4 -4
  15. package/dist/esm/components/wheelpicker/wheelpicker.js +8 -2
  16. package/dist/esm/core/index.js +1 -0
  17. package/dist/esm/core/use-prefers-reduced-motion.js +31 -0
  18. package/dist/esm/product/recordpage/recordpage.js +8 -3
  19. package/dist/esm/product/recordstable/recordstable.js +6 -3
  20. package/dist/esm/product/sidebar/sidebar.js +2 -2
  21. package/dist/types/components/columncalc/columncalc.d.ts +5 -1
  22. package/dist/types/components/multiselect/multiselect.d.ts +3 -1
  23. package/dist/types/components/select/select.d.ts +3 -1
  24. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +9 -1
  25. package/dist/types/core/index.d.ts +1 -0
  26. package/dist/types/core/use-prefers-reduced-motion.d.ts +17 -0
  27. package/dist/types/product/recordpage/recordpage.d.ts +10 -0
  28. package/dist/types/product/recordstable/recordstable.d.ts +6 -0
  29. package/dist/types/product/sidebar/sidebar.d.ts +4 -0
  30. package/package.json +1 -1
@@ -117,7 +117,7 @@ function computeCalc(values, op) {
117
117
  const defaultFormat = (n, op) => op === "percentEmpty" || op === "percentFilled"
118
118
  ? `${Math.round(n)}%`
119
119
  : n.toLocaleString(undefined, { maximumFractionDigits: 2 });
120
- function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, format = defaultFormat, align, opLabels, className, ...rest }) {
120
+ function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel = "Add calculation", sectionLabel = "Aggregations", format = defaultFormat, align, opLabels, className, ...rest }) {
121
121
  // a partial override, so a consumer can translate one op without
122
122
  // restating all nine
123
123
  const labelFor = (o) => opLabels?.[o] ?? exports.CALC_LABEL[o];
@@ -135,5 +135,5 @@ function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opPr
135
135
  ...(numeric ? exports.NUMERIC_CALCS : []),
136
136
  "none",
137
137
  ];
138
- return ((0, jsx_runtime_1.jsx)("span", { className: (0, columncalc_recipe_1.columncalcRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)(menu_1.Menu, { trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "columncalc-trigger", "data-align": align === "right" ? "right" : undefined, children: op === "none" || result === null ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 12, strokeWidth: 2 }), (0, jsx_runtime_1.jsx)("span", { className: "columncalc-placeholder", children: "Add calculation" })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "columncalc-value", children: format(result, op) }), (0, jsx_runtime_1.jsx)("span", { className: "columncalc-op", children: labelFor(op) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 12, strokeWidth: 2 })] })) }), children: [(0, jsx_runtime_1.jsx)("div", { className: "columncalc-section", children: "Aggregations" }), ops.map((o) => ((0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setOp(o), children: labelFor(o) }, o)))] }) }));
138
+ return ((0, jsx_runtime_1.jsx)("span", { className: (0, columncalc_recipe_1.columncalcRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)(menu_1.Menu, { trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "columncalc-trigger", "data-align": align === "right" ? "right" : undefined, children: op === "none" || result === null ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 12, strokeWidth: 2 }), (0, jsx_runtime_1.jsx)("span", { className: "columncalc-placeholder", children: addLabel })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "columncalc-value", children: format(result, op) }), (0, jsx_runtime_1.jsx)("span", { className: "columncalc-op", children: labelFor(op) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 12, strokeWidth: 2 })] })) }), children: [(0, jsx_runtime_1.jsx)("div", { className: "columncalc-section", children: sectionLabel }), ops.map((o) => ((0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setOp(o), children: labelFor(o) }, o)))] }) }));
139
139
  }
@@ -64,7 +64,7 @@ function Chip({ opt }) {
64
64
  * popover with a search field over rows of "[chip] … [checkbox]" that toggle on
65
65
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
66
66
  */
67
- function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
67
+ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
68
68
  // Virtual focus: DOM focus stays on the trigger (or the search field) and the
69
69
  // active option is named by `aria-activedescendant`. `data-highlighted` is a
70
70
  // styling hook only -- a screen reader cannot see it.
@@ -161,7 +161,7 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
161
161
  }, children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
162
162
  setQuery(e.target.value);
163
163
  setHighlight(0);
164
- }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
164
+ }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
165
165
  const sel = selectedSet.has(opt.value);
166
166
  return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
167
167
  }))] }))] }));
@@ -51,7 +51,7 @@ function optionText(o) {
51
51
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
52
52
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
53
53
  */
54
- function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
54
+ function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
55
55
  // Virtual focus: DOM focus stays on the trigger (or the search field), and the
56
56
  // active option is named by `aria-activedescendant`. That is the only thing a
57
57
  // screen reader has to go on -- `data-highlighted` is invisible to it -- so the
@@ -150,7 +150,7 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
150
150
  return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
151
151
  setQuery(e.target.value);
152
152
  setHighlight(0);
153
- }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
153
+ }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
154
154
  const isSel = opt.value === selected;
155
155
  return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
156
156
  }))] }))] }));
@@ -134,7 +134,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
134
134
  * composable version, to put above `DataTable` or any table of your own. Pair
135
135
  * it with `applyFilters` / `applySorts` to turn its state into rows.
136
136
  */
137
- function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel = "Sort", reorderSortLabel = "Reorder sort", sortFieldLabel = "Sort field", sortDirectionLabel = "Sort direction", removeSortLabel = "Remove sort", filterFieldLabel = "Filter by field", filterFieldPlaceholder = "Filter by…", filterValuePlaceholder = "Value…", ascendingLabel = "Ascending", descendingLabel = "Descending", sortTriggerLabel, filterTriggerLabel, ...rest }) {
137
+ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel = "Sort", reorderSortLabel = "Reorder sort", noSortsLabel = "No sorts applied to this view.", addSortLabel = "Add sort", learnSortingLabel = "Learn about sorting", selectConditionLabel = "Select condition", sortFieldLabel = "Sort field", sortDirectionLabel = "Sort direction", removeSortLabel = "Remove sort", filterFieldLabel = "Filter by field", filterFieldPlaceholder = "Filter by…", filterValuePlaceholder = "Value…", ascendingLabel = "Ascending", descendingLabel = "Descending", sortTriggerLabel, filterTriggerLabel, ...rest }) {
138
138
  const [sortsState, setSortsState] = React.useState(defaultSorts ?? []);
139
139
  const [filtersState, setFiltersState] = React.useState(defaultFilters ?? []);
140
140
  const sorts = sortsProp ?? sortsState;
@@ -206,7 +206,7 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
206
206
  : sorts.length
207
207
  ? `Sorted by ${columns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
208
208
  : "Sort";
209
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-toolbar", children: [(0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `tabletoolbar-tool${sorts.length ? " 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", { className: "tabletoolbar-menu tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "tabletoolbar-sort-empty", children: "No sorts applied to this view." })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-sort-row", onDragOver: (e) => {
209
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-toolbar", children: [(0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `tabletoolbar-tool${sorts.length ? " 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", { className: "tabletoolbar-menu tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-sort-row", onDragOver: (e) => {
210
210
  if (dragSort !== null)
211
211
  e.preventDefault();
212
212
  }, onDrop: () => {
@@ -228,7 +228,7 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
228
228
  label: ((0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
229
229
  keywords: descendingLabel,
230
230
  },
231
- ], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "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: "tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), "Add sort"] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), "Learn about sorting"] }) }))] }))] }), (0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `tabletoolbar-tool tabletoolbar-tool--ghost${activeFilters ? " 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
231
+ ], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "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: "tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "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: "tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "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.jsxs)("span", { className: "tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `tabletoolbar-tool tabletoolbar-tool--ghost${activeFilters ? " 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
232
232
  ? filterTriggerLabel(activeFilters)
233
233
  : activeFilters
234
234
  ? `Filter · ${activeFilters}`
@@ -242,7 +242,7 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
242
242
  const col = columns.find((c) => c.key === cond.key);
243
243
  const open = openMenu === `fcond:${cond.id}`;
244
244
  const hasValue = !opNeedsValue(cond.op) || cond.value.trim();
245
- return ((0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "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: "tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-fchip-val", children: cond.value }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-fchip-op", children: "Select condition" }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-menu-head", children: [col?.label, " condition"] }), OP_ORDER.map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
245
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "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: "tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-fchip-val", children: cond.value }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "tabletoolbar-menu-head", children: [col?.label, " condition"] }), OP_ORDER.map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
246
246
  // biome-ignore lint/a11y/noAutofocus: focus the value field
247
247
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) }))] }))] }, cond.id));
248
248
  }), (0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-spacer" }), count != null && (0, jsx_runtime_1.jsx)("span", { className: "tabletoolbar-count", children: count }), children] }) }));
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.WheelPicker = WheelPicker;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const React = __importStar(require("react"));
39
+ const core_1 = require("../../core");
39
40
  const wheelpicker_recipe_1 = require("./wheelpicker.recipe");
40
41
  const ITEM_HEIGHT = { small: 36, medium: 44 };
41
42
  /**
@@ -70,6 +71,11 @@ function WheelPicker({ columns, value: valueProp, defaultValue, onValueChange, s
70
71
  setInternal(merged);
71
72
  onValueChange?.(merged);
72
73
  };
74
+ // CSS cannot reach this: `scroll-behavior` in a stylesheet does not override
75
+ // an explicit `behavior` passed to scrollTo, so the preference is read here
76
+ // and the two "smooth" call sites go through `glide` (#208).
77
+ const reduceMotion = (0, core_1.usePrefersReducedMotion)();
78
+ const glide = reduceMotion ? "auto" : "smooth";
73
79
  const scrollTo = React.useCallback((key, index, behavior) => {
74
80
  const el = refs.current.get(key);
75
81
  if (!el || index < 0)
@@ -107,7 +113,7 @@ function WheelPicker({ columns, value: valueProp, defaultValue, onValueChange, s
107
113
  if (!opt)
108
114
  return;
109
115
  commit(col.key, opt.value);
110
- scrollTo(col.key, clamped, "smooth");
116
+ scrollTo(col.key, clamped, glide);
111
117
  };
112
118
  if (e.key === "ArrowDown")
113
119
  go(i + 1);
@@ -127,7 +133,7 @@ function WheelPicker({ columns, value: valueProp, defaultValue, onValueChange, s
127
133
  const selected = opt.value === value[col.key];
128
134
  return ((0, jsx_runtime_1.jsx)("div", { id: optionId(col.key, opt.value), role: "option", "aria-selected": selected, "data-selected": selected ? "" : undefined, className: "wheelpicker-item", onClick: () => {
129
135
  commit(col.key, opt.value);
130
- scrollTo(col.key, col.options.indexOf(opt), "smooth");
136
+ scrollTo(col.key, col.options.indexOf(opt), glide);
131
137
  }, children: opt.label }, opt.value));
132
138
  }) }, col.key))) }) })] }));
133
139
  }
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.resolveRecipe = exports.Primitive = exports.defineRecipe = exports.cx = void 0;
17
+ exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.defineRecipe = exports.cx = void 0;
18
18
  var cx_1 = require("./cx");
19
19
  Object.defineProperty(exports, "cx", { enumerable: true, get: function () { return cx_1.cx; } });
20
20
  var define_1 = require("./define");
@@ -24,3 +24,5 @@ Object.defineProperty(exports, "Primitive", { enumerable: true, get: function ()
24
24
  var resolve_1 = require("./resolve");
25
25
  Object.defineProperty(exports, "resolveRecipe", { enumerable: true, get: function () { return resolve_1.resolveRecipe; } });
26
26
  __exportStar(require("./rtl"), exports);
27
+ var use_prefers_reduced_motion_1 = require("./use-prefers-reduced-motion");
28
+ Object.defineProperty(exports, "usePrefersReducedMotion", { enumerable: true, get: function () { return use_prefers_reduced_motion_1.usePrefersReducedMotion; } });
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.usePrefersReducedMotion = usePrefersReducedMotion;
37
+ const React = __importStar(require("react"));
38
+ const QUERY = "(prefers-reduced-motion: reduce)";
39
+ const subscribe = (onChange) => {
40
+ if (typeof window === "undefined" || !window.matchMedia)
41
+ return () => { };
42
+ const mq = window.matchMedia(QUERY);
43
+ mq.addEventListener("change", onChange);
44
+ return () => mq.removeEventListener("change", onChange);
45
+ };
46
+ const getSnapshot = () => typeof window !== "undefined" && window.matchMedia
47
+ ? window.matchMedia(QUERY).matches
48
+ : false;
49
+ /**
50
+ * Whether the user has asked for reduced motion, live.
51
+ *
52
+ * crisp's CSS carries a `@media (prefers-reduced-motion: reduce)` override in
53
+ * every animating recipe, and `bun run spec` checks it. That covers CSS.
54
+ * Motion driven from JavaScript — `scrollTo({ behavior: "smooth" })`,
55
+ * `element.animate()` — is invisible to a stylesheet: `scroll-behavior` in CSS
56
+ * does NOT override an explicit `behavior` argument. Those call sites have to
57
+ * read the preference here (#208).
58
+ *
59
+ * The server snapshot is `false` rather than "unknown" on purpose. It is the
60
+ * only value that keeps the markup identical on both sides, and a component
61
+ * using this must not put the result in its output for the same reason — the
62
+ * preference belongs in a handler or an effect, where it changes behaviour
63
+ * rather than HTML.
64
+ */
65
+ function usePrefersReducedMotion() {
66
+ return React.useSyncExternalStore(subscribe, getSnapshot, () => false);
67
+ }
@@ -64,6 +64,7 @@ exports.recordPageLabels = {
64
64
  composeEmail: "Compose email",
65
65
  askAttio: "Ask Attio",
66
66
  addToList: "Add to list",
67
+ noLists: "This record has not been added to any lists.",
67
68
  highlights: "Highlights",
68
69
  recordDetails: "Record Details",
69
70
  searchFields: "Search fields",
@@ -76,6 +77,10 @@ exports.recordPageLabels = {
76
77
  viewLess: "View less",
77
78
  newNote: "New note",
78
79
  newTask: "New task",
80
+ notePlaceholder: "Write a note… (⌘↵ to save)",
81
+ saveNote: "Save note",
82
+ taskPlaceholder: "Task name… (↵ to add)",
83
+ addTask: "Add task",
79
84
  cancel: "Cancel",
80
85
  noNotesYet: "No notes yet.",
81
86
  noTasksYet: "No tasks yet.",
@@ -159,14 +164,14 @@ function Composer({ placeholder, submitLabel, multiline, onSubmit, onCancel, lab
159
164
  }
160
165
  function NotesSection({ notes, onAdd, labels, }) {
161
166
  const [open, setOpen] = React.useState(false);
162
- return ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: labels.tabs.notes, count: notes.length, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), labels.newNote] }) }), open && ((0, jsx_runtime_1.jsx)(Composer, { labels: labels, multiline: true, placeholder: "Write a note\u2026 (\u2318\u21B5 to save)", submitLabel: "Save note", onCancel: () => setOpen(false), onSubmit: (t) => {
167
+ return ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: labels.tabs.notes, count: notes.length, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), labels.newNote] }) }), open && ((0, jsx_runtime_1.jsx)(Composer, { labels: labels, multiline: true, placeholder: labels.notePlaceholder, submitLabel: labels.saveNote, onCancel: () => setOpen(false), onSubmit: (t) => {
163
168
  onAdd(t);
164
169
  setOpen(false);
165
170
  } })), notes.length === 0 && !open ? ((0, jsx_runtime_1.jsx)("div", { className: "recordpage-emptystate", children: labels.noNotesYet })) : (notes.map((n) => ((0, jsx_runtime_1.jsxs)("div", { className: "recordpage-note", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "circle", tone: "neutral", children: "Y" }), (0, jsx_runtime_1.jsxs)("div", { className: "recordpage-note-body", children: [(0, jsx_runtime_1.jsxs)("div", { className: "recordpage-note-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "recordpage-note-author", children: labels.you }), (0, jsx_runtime_1.jsx)("span", { className: "recordpage-note-time", children: n.time })] }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-note-text", children: n.text })] })] }, n.id))))] }));
166
171
  }
167
172
  function TasksSection({ tasks, onAdd, onToggle, labels, }) {
168
173
  const [open, setOpen] = React.useState(false);
169
- return ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: labels.tabs.tasks, count: tasks.length, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), labels.newTask] }) }), open && ((0, jsx_runtime_1.jsx)(Composer, { labels: labels, placeholder: "Task name\u2026 (\u21B5 to add)", submitLabel: "Add task", onCancel: () => setOpen(false), onSubmit: (t) => {
174
+ return ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: labels.tabs.tasks, count: tasks.length, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), labels.newTask] }) }), open && ((0, jsx_runtime_1.jsx)(Composer, { labels: labels, placeholder: labels.taskPlaceholder, submitLabel: labels.addTask, onCancel: () => setOpen(false), onSubmit: (t) => {
170
175
  onAdd(t);
171
176
  setOpen(false);
172
177
  } })), tasks.length === 0 && !open ? ((0, jsx_runtime_1.jsx)("div", { className: "recordpage-emptystate", children: labels.noTasksYet })) : (tasks.map((t) => ((0, jsx_runtime_1.jsxs)("div", { className: "recordpage-task", "data-done": t.done ? "" : undefined, children: [(0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { checked: t.done, onChange: () => onToggle(t.id), "aria-label": t.text }), (0, jsx_runtime_1.jsx)("span", { className: "recordpage-task-text", children: t.text })] }, t.id))))] }));
@@ -399,7 +404,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
399
404
  setSearchOpen(next);
400
405
  if (!next)
401
406
  setFieldQuery("");
402
- }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), (0, jsx_runtime_1.jsxs)("div", { className: "recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-emptystate", children: "This record has not been added to any lists." })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
407
+ }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), (0, jsx_runtime_1.jsxs)("div", { className: "recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
403
408
  if (e.key === "ArrowLeft")
404
409
  setDetailsW((w) => Math.max(300, w - 16));
405
410
  else if (e.key === "ArrowRight")
@@ -124,6 +124,9 @@ exports.recordsTableLabels = {
124
124
  duplicate: "Duplicate",
125
125
  addToList: "Add to list",
126
126
  inAList: "In a list",
127
+ searchRecordsPlaceholder: "Search records…",
128
+ searchOptionsPlaceholder: "Search options…",
129
+ selectedCount: "Selected",
127
130
  delete: "Delete",
128
131
  sendEmail: "Send email",
129
132
  exportCsv: "Export CSV",
@@ -309,14 +312,14 @@ function StatusCell({ column, row, onChange, }) {
309
312
  return ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": column.label, placeholder: "\u2014", value: row[column.key] ?? undefined, onValueChange: onChange, options: column.options ?? [] }));
310
313
  }
311
314
  function ReferenceCell({ column, row, onChange, labels, }) {
312
- return ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, searchPlaceholder: "Search records\u2026", "aria-label": column.label, placeholder: labels.setRecordPlaceholder, value: row[column.key] ?? undefined, onValueChange: onChange, options: (column.refOptions ?? []).map((o) => ({
315
+ return ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, searchPlaceholder: labels.searchRecordsPlaceholder, "aria-label": column.label, placeholder: labels.setRecordPlaceholder, value: row[column.key] ?? undefined, onValueChange: onChange, options: (column.refOptions ?? []).map((o) => ({
313
316
  value: o.value,
314
317
  label: o.label,
315
318
  })) }));
316
319
  }
317
320
  function TagsCell({ column, row, options, onChange, labels, }) {
318
321
  const value = row[column.key] ?? [];
319
- return ((0, jsx_runtime_1.jsx)(multiselect_1.MultiSelect, { options: options, value: value, onValueChange: onChange, placeholder: labels.emptyPlaceholder, searchPlaceholder: "Search options\u2026", "aria-label": column.label }));
322
+ return ((0, jsx_runtime_1.jsx)(multiselect_1.MultiSelect, { options: options, value: value, onValueChange: onChange, placeholder: labels.emptyPlaceholder, searchPlaceholder: labels.searchOptionsPlaceholder, "aria-label": column.label }));
320
323
  }
321
324
  function DateCell({ column, row, onChange, }) {
322
325
  const raw = row[column.key];
@@ -1047,7 +1050,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1047
1050
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--up", role: "menu", "aria-label": `${col.label} summary`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: aggOptions(col).map((opt) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitemradio", tabIndex: -1, "aria-checked": a === opt, "data-on": a === opt ? "" : undefined, onClick: () => setAggValue(col.key, opt), children: [t.agg[opt], a === opt ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
1048
1051
  }), (0, jsx_runtime_1.jsx)("td", { className: "recordstable-sum recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "recordstable-empty", children: (0, jsx_runtime_1.jsx)(emptystate_1.EmptyState, { variant: "page", icon: activeFilters ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Inbox, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1049
1052
  ? "None of your records match the filters that are set."
1050
- : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: "Selected", onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), (0, jsx_runtime_1.jsx)(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1053
+ : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), (0, jsx_runtime_1.jsx)(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1051
1054
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: ctxRef, className: "recordstable-menu recordstable-ctx", role: "menu", "aria-label": t.recordActions, style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, onKeyDown: onCtxKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
1052
1055
  }
1053
1056
  exports.defaultColumns = [
@@ -117,7 +117,7 @@ exports.ATTIO_SIDEBAR_FOOTER = {
117
117
  function SectionHeader({ label, open, onToggle, }) {
118
118
  return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [(0, jsx_runtime_1.jsx)(IconChevronMini, {}), (0, jsx_runtime_1.jsx)("span", { children: label })] }));
119
119
  }
120
- exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, footer = exports.ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
120
+ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, footer = exports.ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
121
121
  const [collapsedSections, setCollapsedSections] = React.useState(() => new Set());
122
122
  const [commandOpen, setCommandOpen] = React.useState(false);
123
123
  React.useEffect(() => {
@@ -143,6 +143,6 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
143
143
  return next;
144
144
  });
145
145
  }, []);
146
- return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.div, { ref: ref, asChild: asChild, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {}), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "sidebar-search-label", children: "Search" }), (0, jsx_runtime_1.jsx)("span", { className: "sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), children: label }, label))) }), (0, jsx_runtime_1.jsxs)("div", { className: "sidebar-footer", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", className: "sidebar-pill", children: footer.gettingStartedLabel }), (0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(IconInvite, {}), children: footer.inviteLabel }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-divider" }), (0, jsx_runtime_1.jsxs)("div", { className: "sidebar-trial", children: [(0, jsx_runtime_1.jsx)("span", { children: footer.trialText }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), (0, jsx_runtime_1.jsx)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
146
+ return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.div, { ref: ref, asChild: asChild, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {}), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), children: label }, label))) }), (0, jsx_runtime_1.jsxs)("div", { className: "sidebar-footer", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", className: "sidebar-pill", children: footer.gettingStartedLabel }), (0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(IconInvite, {}), children: footer.inviteLabel }), (0, jsx_runtime_1.jsx)("div", { className: "sidebar-divider" }), (0, jsx_runtime_1.jsxs)("div", { className: "sidebar-trial", children: [(0, jsx_runtime_1.jsx)("span", { children: footer.trialText }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), (0, jsx_runtime_1.jsx)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
147
147
  });
148
148
  exports.Sidebar.displayName = "Sidebar";
@@ -78,7 +78,7 @@ export function computeCalc(values, op) {
78
78
  const defaultFormat = (n, op) => op === "percentEmpty" || op === "percentFilled"
79
79
  ? `${Math.round(n)}%`
80
80
  : n.toLocaleString(undefined, { maximumFractionDigits: 2 });
81
- export function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, format = defaultFormat, align, opLabels, className, ...rest }) {
81
+ export function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel = "Add calculation", sectionLabel = "Aggregations", format = defaultFormat, align, opLabels, className, ...rest }) {
82
82
  // a partial override, so a consumer can translate one op without
83
83
  // restating all nine
84
84
  const labelFor = (o) => opLabels?.[o] ?? CALC_LABEL[o];
@@ -96,5 +96,5 @@ export function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", o
96
96
  ...(numeric ? NUMERIC_CALCS : []),
97
97
  "none",
98
98
  ];
99
- return (_jsx("span", { className: columncalcRecipe({ className }), ...rest, children: _jsxs(Menu, { trigger: _jsx("button", { type: "button", className: "columncalc-trigger", "data-align": align === "right" ? "right" : undefined, children: op === "none" || result === null ? (_jsxs(_Fragment, { children: [_jsx(Plus, { size: 12, strokeWidth: 2 }), _jsx("span", { className: "columncalc-placeholder", children: "Add calculation" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "columncalc-value", children: format(result, op) }), _jsx("span", { className: "columncalc-op", children: labelFor(op) }), _jsx(ChevronDown, { size: 12, strokeWidth: 2 })] })) }), children: [_jsx("div", { className: "columncalc-section", children: "Aggregations" }), ops.map((o) => (_jsx(MenuItem, { onSelect: () => setOp(o), children: labelFor(o) }, o)))] }) }));
99
+ return (_jsx("span", { className: columncalcRecipe({ className }), ...rest, children: _jsxs(Menu, { trigger: _jsx("button", { type: "button", className: "columncalc-trigger", "data-align": align === "right" ? "right" : undefined, children: op === "none" || result === null ? (_jsxs(_Fragment, { children: [_jsx(Plus, { size: 12, strokeWidth: 2 }), _jsx("span", { className: "columncalc-placeholder", children: addLabel })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "columncalc-value", children: format(result, op) }), _jsx("span", { className: "columncalc-op", children: labelFor(op) }), _jsx(ChevronDown, { size: 12, strokeWidth: 2 })] })) }), children: [_jsx("div", { className: "columncalc-section", children: sectionLabel }), ops.map((o) => (_jsx(MenuItem, { onSelect: () => setOp(o), children: labelFor(o) }, o)))] }) }));
100
100
  }
@@ -28,7 +28,7 @@ function Chip({ opt }) {
28
28
  * popover with a search field over rows of "[chip] … [checkbox]" that toggle on
29
29
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
30
30
  */
31
- export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
31
+ export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
32
32
  // Virtual focus: DOM focus stays on the trigger (or the search field) and the
33
33
  // active option is named by `aria-activedescendant`. `data-highlighted` is a
34
34
  // styling hook only -- a screen reader cannot see it.
@@ -125,7 +125,7 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
125
125
  }, children: [_jsx("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
126
126
  setQuery(e.target.value);
127
127
  setHighlight(0);
128
- }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
128
+ }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
129
129
  const sel = selectedSet.has(opt.value);
130
130
  return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
131
131
  }))] }))] }));
@@ -15,7 +15,7 @@ function optionText(o) {
15
15
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
16
16
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
17
17
  */
18
- export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
18
+ export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
19
19
  // Virtual focus: DOM focus stays on the trigger (or the search field), and the
20
20
  // active option is named by `aria-activedescendant`. That is the only thing a
21
21
  // screen reader has to go on -- `data-highlighted` is invisible to it -- so the
@@ -114,7 +114,7 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
114
114
  return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
115
115
  setQuery(e.target.value);
116
116
  setHighlight(0);
117
- }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
117
+ }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
118
118
  const isSel = opt.value === selected;
119
119
  return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
120
120
  }))] }))] }));
@@ -96,7 +96,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
96
96
  * composable version, to put above `DataTable` or any table of your own. Pair
97
97
  * it with `applyFilters` / `applySorts` to turn its state into rows.
98
98
  */
99
- export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel = "Sort", reorderSortLabel = "Reorder sort", sortFieldLabel = "Sort field", sortDirectionLabel = "Sort direction", removeSortLabel = "Remove sort", filterFieldLabel = "Filter by field", filterFieldPlaceholder = "Filter by…", filterValuePlaceholder = "Value…", ascendingLabel = "Ascending", descendingLabel = "Descending", sortTriggerLabel, filterTriggerLabel, ...rest }) {
99
+ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel = "Sort", reorderSortLabel = "Reorder sort", noSortsLabel = "No sorts applied to this view.", addSortLabel = "Add sort", learnSortingLabel = "Learn about sorting", selectConditionLabel = "Select condition", sortFieldLabel = "Sort field", sortDirectionLabel = "Sort direction", removeSortLabel = "Remove sort", filterFieldLabel = "Filter by field", filterFieldPlaceholder = "Filter by…", filterValuePlaceholder = "Value…", ascendingLabel = "Ascending", descendingLabel = "Descending", sortTriggerLabel, filterTriggerLabel, ...rest }) {
100
100
  const [sortsState, setSortsState] = React.useState(defaultSorts ?? []);
101
101
  const [filtersState, setFiltersState] = React.useState(defaultFilters ?? []);
102
102
  const sorts = sortsProp ?? sortsState;
@@ -168,7 +168,7 @@ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsC
168
168
  : sorts.length
169
169
  ? `Sorted by ${columns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
170
170
  : "Sort";
171
- return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "tabletoolbar-toolbar", children: [_jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `tabletoolbar-tool${sorts.length ? " 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", { className: "tabletoolbar-menu tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "tabletoolbar-sort-empty", children: "No sorts applied to this view." })) : (sorts.map((s, i) => (_jsxs("div", { className: "tabletoolbar-sort-row", onDragOver: (e) => {
171
+ return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "tabletoolbar-toolbar", children: [_jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `tabletoolbar-tool${sorts.length ? " 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", { className: "tabletoolbar-menu tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "tabletoolbar-sort-row", onDragOver: (e) => {
172
172
  if (dragSort !== null)
173
173
  e.preventDefault();
174
174
  }, onDrop: () => {
@@ -190,7 +190,7 @@ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsC
190
190
  label: (_jsxs("span", { className: "tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
191
191
  keywords: descendingLabel,
192
192
  },
193
- ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), "Add sort"] }) }), helpHref && (_jsx("div", { className: "tabletoolbar-sort-foot", children: _jsxs("a", { className: "tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), "Learn about sorting"] }) }))] }))] }), _jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `tabletoolbar-tool tabletoolbar-tool--ghost${activeFilters ? " 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
193
+ ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "tabletoolbar-sort-foot", children: _jsxs("a", { className: "tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), _jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `tabletoolbar-tool tabletoolbar-tool--ghost${activeFilters ? " 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
194
194
  ? filterTriggerLabel(activeFilters)
195
195
  : activeFilters
196
196
  ? `Filter · ${activeFilters}`
@@ -204,7 +204,7 @@ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsC
204
204
  const col = columns.find((c) => c.key === cond.key);
205
205
  const open = openMenu === `fcond:${cond.id}`;
206
206
  const hasValue = !opNeedsValue(cond.op) || cond.value.trim();
207
- return (_jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("span", { className: "tabletoolbar-fchip", children: [_jsxs("button", { type: "button", className: "tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "tabletoolbar-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "tabletoolbar-fchip-val", children: cond.value }))] })) : (_jsx("span", { className: "tabletoolbar-fchip-op", children: "Select condition" }))] }), _jsx("button", { type: "button", className: "tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }), open && (_jsxs("div", { className: "tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [_jsxs("div", { className: "tabletoolbar-menu-head", children: [col?.label, " condition"] }), OP_ORDER.map((op) => (_jsxs("button", { type: "button", className: "tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
207
+ return (_jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("span", { className: "tabletoolbar-fchip", children: [_jsxs("button", { type: "button", className: "tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "tabletoolbar-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "tabletoolbar-fchip-val", children: cond.value }))] })) : (_jsx("span", { className: "tabletoolbar-fchip-op", children: selectConditionLabel }))] }), _jsx("button", { type: "button", className: "tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }), open && (_jsxs("div", { className: "tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [_jsxs("div", { className: "tabletoolbar-menu-head", children: [col?.label, " condition"] }), OP_ORDER.map((op) => (_jsxs("button", { type: "button", className: "tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
208
208
  // biome-ignore lint/a11y/noAutofocus: focus the value field
209
209
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) }))] }))] }, cond.id));
210
210
  }), _jsx("span", { className: "tabletoolbar-spacer" }), count != null && _jsx("span", { className: "tabletoolbar-count", children: count }), children] }) }));
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { usePrefersReducedMotion } from "../../core/index.js";
3
4
  import { wheelpickerRecipe } from "./wheelpicker.recipe.js";
4
5
  const ITEM_HEIGHT = { small: 36, medium: 44 };
5
6
  /**
@@ -34,6 +35,11 @@ export function WheelPicker({ columns, value: valueProp, defaultValue, onValueCh
34
35
  setInternal(merged);
35
36
  onValueChange?.(merged);
36
37
  };
38
+ // CSS cannot reach this: `scroll-behavior` in a stylesheet does not override
39
+ // an explicit `behavior` passed to scrollTo, so the preference is read here
40
+ // and the two "smooth" call sites go through `glide` (#208).
41
+ const reduceMotion = usePrefersReducedMotion();
42
+ const glide = reduceMotion ? "auto" : "smooth";
37
43
  const scrollTo = React.useCallback((key, index, behavior) => {
38
44
  const el = refs.current.get(key);
39
45
  if (!el || index < 0)
@@ -71,7 +77,7 @@ export function WheelPicker({ columns, value: valueProp, defaultValue, onValueCh
71
77
  if (!opt)
72
78
  return;
73
79
  commit(col.key, opt.value);
74
- scrollTo(col.key, clamped, "smooth");
80
+ scrollTo(col.key, clamped, glide);
75
81
  };
76
82
  if (e.key === "ArrowDown")
77
83
  go(i + 1);
@@ -91,7 +97,7 @@ export function WheelPicker({ columns, value: valueProp, defaultValue, onValueCh
91
97
  const selected = opt.value === value[col.key];
92
98
  return (_jsx("div", { id: optionId(col.key, opt.value), role: "option", "aria-selected": selected, "data-selected": selected ? "" : undefined, className: "wheelpicker-item", onClick: () => {
93
99
  commit(col.key, opt.value);
94
- scrollTo(col.key, col.options.indexOf(opt), "smooth");
100
+ scrollTo(col.key, col.options.indexOf(opt), glide);
95
101
  }, children: opt.label }, opt.value));
96
102
  }) }, col.key))) }) })] }));
97
103
  }
@@ -3,3 +3,4 @@ export { defineRecipe } from "./define.js";
3
3
  export { Primitive } from "./primitive.js";
4
4
  export { resolveRecipe } from "./resolve.js";
5
5
  export * from "./rtl.js";
6
+ export { usePrefersReducedMotion } from "./use-prefers-reduced-motion.js";
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ const QUERY = "(prefers-reduced-motion: reduce)";
3
+ const subscribe = (onChange) => {
4
+ if (typeof window === "undefined" || !window.matchMedia)
5
+ return () => { };
6
+ const mq = window.matchMedia(QUERY);
7
+ mq.addEventListener("change", onChange);
8
+ return () => mq.removeEventListener("change", onChange);
9
+ };
10
+ const getSnapshot = () => typeof window !== "undefined" && window.matchMedia
11
+ ? window.matchMedia(QUERY).matches
12
+ : false;
13
+ /**
14
+ * Whether the user has asked for reduced motion, live.
15
+ *
16
+ * crisp's CSS carries a `@media (prefers-reduced-motion: reduce)` override in
17
+ * every animating recipe, and `bun run spec` checks it. That covers CSS.
18
+ * Motion driven from JavaScript — `scrollTo({ behavior: "smooth" })`,
19
+ * `element.animate()` — is invisible to a stylesheet: `scroll-behavior` in CSS
20
+ * does NOT override an explicit `behavior` argument. Those call sites have to
21
+ * read the preference here (#208).
22
+ *
23
+ * The server snapshot is `false` rather than "unknown" on purpose. It is the
24
+ * only value that keeps the markup identical on both sides, and a component
25
+ * using this must not put the result in its output for the same reason — the
26
+ * preference belongs in a handler or an effect, where it changes behaviour
27
+ * rather than HTML.
28
+ */
29
+ export function usePrefersReducedMotion() {
30
+ return React.useSyncExternalStore(subscribe, getSnapshot, () => false);
31
+ }
@@ -27,6 +27,7 @@ export const recordPageLabels = {
27
27
  composeEmail: "Compose email",
28
28
  askAttio: "Ask Attio",
29
29
  addToList: "Add to list",
30
+ noLists: "This record has not been added to any lists.",
30
31
  highlights: "Highlights",
31
32
  recordDetails: "Record Details",
32
33
  searchFields: "Search fields",
@@ -39,6 +40,10 @@ export const recordPageLabels = {
39
40
  viewLess: "View less",
40
41
  newNote: "New note",
41
42
  newTask: "New task",
43
+ notePlaceholder: "Write a note… (⌘↵ to save)",
44
+ saveNote: "Save note",
45
+ taskPlaceholder: "Task name… (↵ to add)",
46
+ addTask: "Add task",
42
47
  cancel: "Cancel",
43
48
  noNotesYet: "No notes yet.",
44
49
  noTasksYet: "No tasks yet.",
@@ -122,14 +127,14 @@ function Composer({ placeholder, submitLabel, multiline, onSubmit, onCancel, lab
122
127
  }
123
128
  function NotesSection({ notes, onAdd, labels, }) {
124
129
  const [open, setOpen] = React.useState(false);
125
- return (_jsxs("section", { children: [_jsx(SectionHead, { title: labels.tabs.notes, count: notes.length, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), labels.newNote] }) }), open && (_jsx(Composer, { labels: labels, multiline: true, placeholder: "Write a note\u2026 (\u2318\u21B5 to save)", submitLabel: "Save note", onCancel: () => setOpen(false), onSubmit: (t) => {
130
+ return (_jsxs("section", { children: [_jsx(SectionHead, { title: labels.tabs.notes, count: notes.length, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), labels.newNote] }) }), open && (_jsx(Composer, { labels: labels, multiline: true, placeholder: labels.notePlaceholder, submitLabel: labels.saveNote, onCancel: () => setOpen(false), onSubmit: (t) => {
126
131
  onAdd(t);
127
132
  setOpen(false);
128
133
  } })), notes.length === 0 && !open ? (_jsx("div", { className: "recordpage-emptystate", children: labels.noNotesYet })) : (notes.map((n) => (_jsxs("div", { className: "recordpage-note", children: [_jsx(Avatar, { size: "sm", shape: "circle", tone: "neutral", children: "Y" }), _jsxs("div", { className: "recordpage-note-body", children: [_jsxs("div", { className: "recordpage-note-head", children: [_jsx("span", { className: "recordpage-note-author", children: labels.you }), _jsx("span", { className: "recordpage-note-time", children: n.time })] }), _jsx("div", { className: "recordpage-note-text", children: n.text })] })] }, n.id))))] }));
129
134
  }
130
135
  function TasksSection({ tasks, onAdd, onToggle, labels, }) {
131
136
  const [open, setOpen] = React.useState(false);
132
- return (_jsxs("section", { children: [_jsx(SectionHead, { title: labels.tabs.tasks, count: tasks.length, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), labels.newTask] }) }), open && (_jsx(Composer, { labels: labels, placeholder: "Task name\u2026 (\u21B5 to add)", submitLabel: "Add task", onCancel: () => setOpen(false), onSubmit: (t) => {
137
+ return (_jsxs("section", { children: [_jsx(SectionHead, { title: labels.tabs.tasks, count: tasks.length, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", onClick: () => setOpen(true), children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), labels.newTask] }) }), open && (_jsx(Composer, { labels: labels, placeholder: labels.taskPlaceholder, submitLabel: labels.addTask, onCancel: () => setOpen(false), onSubmit: (t) => {
133
138
  onAdd(t);
134
139
  setOpen(false);
135
140
  } })), tasks.length === 0 && !open ? (_jsx("div", { className: "recordpage-emptystate", children: labels.noTasksYet })) : (tasks.map((t) => (_jsxs("div", { className: "recordpage-task", "data-done": t.done ? "" : undefined, children: [_jsx(Checkbox, { checked: t.done, onChange: () => onToggle(t.id), "aria-label": t.text }), _jsx("span", { className: "recordpage-task-text", children: t.text })] }, t.id))))] }));
@@ -362,7 +367,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
362
367
  setSearchOpen(next);
363
368
  if (!next)
364
369
  setFieldQuery("");
365
- }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsxs("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), _jsxs("div", { className: "recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "recordpage-emptystate", children: "This record has not been added to any lists." })] })] }), _jsx("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
370
+ }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsxs("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), _jsxs("div", { className: "recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
366
371
  if (e.key === "ArrowLeft")
367
372
  setDetailsW((w) => Math.max(300, w - 16));
368
373
  else if (e.key === "ArrowRight")
@@ -86,6 +86,9 @@ export const recordsTableLabels = {
86
86
  duplicate: "Duplicate",
87
87
  addToList: "Add to list",
88
88
  inAList: "In a list",
89
+ searchRecordsPlaceholder: "Search records…",
90
+ searchOptionsPlaceholder: "Search options…",
91
+ selectedCount: "Selected",
89
92
  delete: "Delete",
90
93
  sendEmail: "Send email",
91
94
  exportCsv: "Export CSV",
@@ -271,14 +274,14 @@ function StatusCell({ column, row, onChange, }) {
271
274
  return (_jsx(Select, { variant: "ghost", "aria-label": column.label, placeholder: "\u2014", value: row[column.key] ?? undefined, onValueChange: onChange, options: column.options ?? [] }));
272
275
  }
273
276
  function ReferenceCell({ column, row, onChange, labels, }) {
274
- return (_jsx(Select, { variant: "ghost", searchable: true, searchPlaceholder: "Search records\u2026", "aria-label": column.label, placeholder: labels.setRecordPlaceholder, value: row[column.key] ?? undefined, onValueChange: onChange, options: (column.refOptions ?? []).map((o) => ({
277
+ return (_jsx(Select, { variant: "ghost", searchable: true, searchPlaceholder: labels.searchRecordsPlaceholder, "aria-label": column.label, placeholder: labels.setRecordPlaceholder, value: row[column.key] ?? undefined, onValueChange: onChange, options: (column.refOptions ?? []).map((o) => ({
275
278
  value: o.value,
276
279
  label: o.label,
277
280
  })) }));
278
281
  }
279
282
  function TagsCell({ column, row, options, onChange, labels, }) {
280
283
  const value = row[column.key] ?? [];
281
- return (_jsx(MultiSelect, { options: options, value: value, onValueChange: onChange, placeholder: labels.emptyPlaceholder, searchPlaceholder: "Search options\u2026", "aria-label": column.label }));
284
+ return (_jsx(MultiSelect, { options: options, value: value, onValueChange: onChange, placeholder: labels.emptyPlaceholder, searchPlaceholder: labels.searchOptionsPlaceholder, "aria-label": column.label }));
282
285
  }
283
286
  function DateCell({ column, row, onChange, }) {
284
287
  const raw = row[column.key];
@@ -1009,7 +1012,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1009
1012
  createPortal(_jsx("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--up", role: "menu", "aria-label": `${col.label} summary`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: aggOptions(col).map((opt) => (_jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitemradio", tabIndex: -1, "aria-checked": a === opt, "data-on": a === opt ? "" : undefined, onClick: () => setAggValue(col.key, opt), children: [t.agg[opt], a === opt ? (_jsx(Check, { className: "recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
1010
1013
  }), _jsx("td", { className: "recordstable-sum recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? (_jsx("div", { className: "recordstable-empty", children: _jsx(EmptyState, { variant: "page", icon: activeFilters ? (_jsx(ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : (_jsx(Inbox, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1011
1014
  ? "None of your records match the filters that are set."
1012
- : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : (_jsxs("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: "Selected", onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1015
+ : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : (_jsxs("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1013
1016
  createPortal(_jsxs("div", { ref: ctxRef, className: "recordstable-menu recordstable-ctx", role: "menu", "aria-label": t.recordActions, style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, onKeyDown: onCtxKeyDown, children: [_jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [_jsx(Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [_jsx(CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [_jsx(Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), _jsxs("button", { type: "button", className: "recordstable-menu-item recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [_jsx(Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
1014
1017
  }
1015
1018
  export const defaultColumns = [
@@ -81,7 +81,7 @@ export const ATTIO_SIDEBAR_FOOTER = {
81
81
  function SectionHeader({ label, open, onToggle, }) {
82
82
  return (_jsxs("button", { type: "button", className: "sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [_jsx(IconChevronMini, {}), _jsx("span", { children: label })] }));
83
83
  }
84
- export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
84
+ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
85
85
  const [collapsedSections, setCollapsedSections] = React.useState(() => new Set());
86
86
  const [commandOpen, setCommandOpen] = React.useState(false);
87
87
  React.useEffect(() => {
@@ -107,6 +107,6 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
107
107
  return next;
108
108
  });
109
109
  }, []);
110
- return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "sidebar-ws", children: [_jsx(Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), _jsx("span", { className: "sidebar-ws-name", children: workspace.name }), _jsx(IconChevronDown, {}), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "sidebar-search-label", children: "Search" }), _jsx("span", { className: "sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "sidebar-divider" }), _jsx("div", { className: "sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }), _jsx(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), _jsx("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => (_jsx(NavItem, { href: "#", icon: _jsx(Avatar, { size: "sm", shape: "rounded", tone: tone, children: _jsx(Glyph, {}) }), children: label }, label))) }), _jsx(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), _jsx("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: _jsx("button", { type: "button", className: "sidebar-newlist", children: listsSection.newListLabel }) }), _jsx(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), _jsx("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => (_jsx(NavItem, { href: "#", icon: _jsx(ItemIcon, {}), children: label }, label))) }), _jsxs("div", { className: "sidebar-footer", children: [_jsx(Button, { intent: "neutral", size: "sm", className: "sidebar-pill", children: footer.gettingStartedLabel }), _jsx(NavItem, { href: "#", icon: _jsx(IconInvite, {}), children: footer.inviteLabel }), _jsx("div", { className: "sidebar-divider" }), _jsxs("div", { className: "sidebar-trial", children: [_jsx("span", { children: footer.trialText }), _jsx(Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), _jsx(Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
110
+ return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "sidebar-ws", children: [_jsx(Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), _jsx("span", { className: "sidebar-ws-name", children: workspace.name }), _jsx(IconChevronDown, {}), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "sidebar-search-label", children: searchLabel }), _jsx("span", { className: "sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "sidebar-divider" }), _jsx("div", { className: "sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }), _jsx(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), _jsx("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => (_jsx(NavItem, { href: "#", icon: _jsx(Avatar, { size: "sm", shape: "rounded", tone: tone, children: _jsx(Glyph, {}) }), children: label }, label))) }), _jsx(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), _jsx("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: _jsx("button", { type: "button", className: "sidebar-newlist", children: listsSection.newListLabel }) }), _jsx(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), _jsx("div", { className: "sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => (_jsx(NavItem, { href: "#", icon: _jsx(ItemIcon, {}), children: label }, label))) }), _jsxs("div", { className: "sidebar-footer", children: [_jsx(Button, { intent: "neutral", size: "sm", className: "sidebar-pill", children: footer.gettingStartedLabel }), _jsx(NavItem, { href: "#", icon: _jsx(IconInvite, {}), children: footer.inviteLabel }), _jsx("div", { className: "sidebar-divider" }), _jsxs("div", { className: "sidebar-trial", children: [_jsx("span", { children: footer.trialText }), _jsx(Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), _jsx(Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
111
111
  });
112
112
  Sidebar.displayName = "Sidebar";
@@ -37,6 +37,10 @@ export type ColumnCalcProps<T> = Omit<React.ComponentPropsWithoutRef<"span">, "v
37
37
  /** Controlled operation. */
38
38
  op?: CalcOp;
39
39
  onOpChange?: (op: CalcOp) => void;
40
+ /** Placeholder in the cell when no calculation is chosen. */
41
+ addLabel?: string;
42
+ /** Heading above the operation list in the menu. */
43
+ sectionLabel?: string;
40
44
  /** Overrides the operation names shown in the menu and the cell. Partial —
41
45
  * anything omitted keeps its English default from `CALC_LABEL`. */
42
46
  opLabels?: Partial<Record<CalcOp, string>>;
@@ -46,4 +50,4 @@ export type ColumnCalcProps<T> = Omit<React.ComponentPropsWithoutRef<"span">, "v
46
50
  align?: "left" | "right";
47
51
  className?: string;
48
52
  };
49
- export declare function ColumnCalc<T>({ rows, value, numeric, defaultOp, op: opProp, onOpChange, format, align, opLabels, className, ...rest }: ColumnCalcProps<T>): React.JSX.Element;
53
+ export declare function ColumnCalc<T>({ rows, value, numeric, defaultOp, op: opProp, onOpChange, addLabel, sectionLabel, format, align, opLabels, className, ...rest }: ColumnCalcProps<T>): React.JSX.Element;
@@ -17,6 +17,8 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
17
17
  /** Show a filter field at the top of the list (default true). */
18
18
  searchable?: boolean;
19
19
  searchPlaceholder?: string;
20
+ /** Shown when the search matches nothing. */
21
+ emptyLabel?: string;
20
22
  disabled?: boolean;
21
23
  className?: string;
22
24
  "aria-label"?: string;
@@ -27,7 +29,7 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
27
29
  * popover with a search field over rows of "[chip] … [checkbox]" that toggle on
28
30
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
29
31
  */
30
- export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder, searchable, searchPlaceholder, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
32
+ export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
31
33
  export declare namespace MultiSelect {
32
34
  var displayName: string;
33
35
  }
@@ -22,6 +22,8 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
22
22
  /** Show a search field at the top of the list that filters options (Linear/cmdk style). */
23
23
  searchable?: boolean;
24
24
  searchPlaceholder?: string;
25
+ /** Shown when the search matches nothing. */
26
+ emptyLabel?: string;
25
27
  disabled?: boolean;
26
28
  className?: string;
27
29
  "aria-label"?: string;
@@ -34,7 +36,7 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
34
36
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
35
37
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
36
38
  */
37
- export declare function Select({ options, value, defaultValue, onValueChange, placeholder, variant, width, searchable, searchPlaceholder, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
39
+ export declare function Select({ options, value, defaultValue, onValueChange, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
38
40
  export declare namespace Select {
39
41
  var displayName: string;
40
42
  }
@@ -36,6 +36,14 @@ export type TableToolbarProps = Omit<React.ComponentPropsWithoutRef<"div">, "chi
36
36
  count?: React.ReactNode;
37
37
  /** Right-aligned actions after the count (a "New" button, say). */
38
38
  children?: React.ReactNode;
39
+ /** Empty state in the sort popover. */
40
+ noSortsLabel?: string;
41
+ /** Label on the button that appends a sort condition. */
42
+ addSortLabel?: string;
43
+ /** Text of the help link, when `helpHref` is set. */
44
+ learnSortingLabel?: string;
45
+ /** Placeholder on a filter condition's operator picker. */
46
+ selectConditionLabel?: string;
39
47
  /** Shows a "Learn about sorting" link in the sort popover when set. */
40
48
  helpHref?: string;
41
49
  /** aria-label on the Sort trigger (default "Sort"). */
@@ -85,5 +93,5 @@ export declare function applySorts<T>(rows: T[], sorts: ToolbarSort[], get?: (ro
85
93
  * composable version, to put above `DataTable` or any table of your own. Pair
86
94
  * it with `applyFilters` / `applySorts` to turn its state into rows.
87
95
  */
88
- export declare function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel, reorderSortLabel, sortFieldLabel, sortDirectionLabel, removeSortLabel, filterFieldLabel, filterFieldPlaceholder, filterValuePlaceholder, ascendingLabel, descendingLabel, sortTriggerLabel, filterTriggerLabel, ...rest }: TableToolbarProps): React.JSX.Element;
96
+ export declare function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel, reorderSortLabel, noSortsLabel, addSortLabel, learnSortingLabel, selectConditionLabel, sortFieldLabel, sortDirectionLabel, removeSortLabel, filterFieldLabel, filterFieldPlaceholder, filterValuePlaceholder, ascendingLabel, descendingLabel, sortTriggerLabel, filterTriggerLabel, ...rest }: TableToolbarProps): React.JSX.Element;
89
97
  export {};
@@ -4,3 +4,4 @@ export { Primitive } from "./primitive";
4
4
  export { resolveRecipe } from "./resolve";
5
5
  export * from "./rtl";
6
6
  export type * from "./types";
7
+ export { usePrefersReducedMotion } from "./use-prefers-reduced-motion";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Whether the user has asked for reduced motion, live.
3
+ *
4
+ * crisp's CSS carries a `@media (prefers-reduced-motion: reduce)` override in
5
+ * every animating recipe, and `bun run spec` checks it. That covers CSS.
6
+ * Motion driven from JavaScript — `scrollTo({ behavior: "smooth" })`,
7
+ * `element.animate()` — is invisible to a stylesheet: `scroll-behavior` in CSS
8
+ * does NOT override an explicit `behavior` argument. Those call sites have to
9
+ * read the preference here (#208).
10
+ *
11
+ * The server snapshot is `false` rather than "unknown" on purpose. It is the
12
+ * only value that keeps the markup identical on both sides, and a component
13
+ * using this must not put the result in its output for the same reason — the
14
+ * preference belongs in a handler or an effect, where it changes behaviour
15
+ * rather than HTML.
16
+ */
17
+ export declare function usePrefersReducedMotion(): boolean;
@@ -40,6 +40,8 @@ export type RecordPageLabels = {
40
40
  composeEmail: string;
41
41
  askAttio: string;
42
42
  addToList: string;
43
+ /** Empty state under the Lists panel. */
44
+ noLists: string;
43
45
  highlights: string;
44
46
  recordDetails: string;
45
47
  searchFields: string;
@@ -52,6 +54,14 @@ export type RecordPageLabels = {
52
54
  viewLess: string;
53
55
  newNote: string;
54
56
  newTask: string;
57
+ /** Placeholder in the note composer. */
58
+ notePlaceholder: string;
59
+ /** Submit button in the note composer. */
60
+ saveNote: string;
61
+ /** Placeholder in the task composer. */
62
+ taskPlaceholder: string;
63
+ /** Submit button in the task composer. */
64
+ addTask: string;
55
65
  cancel: string;
56
66
  noNotesYet: string;
57
67
  noTasksYet: string;
@@ -146,6 +146,12 @@ export type RecordsTableLabels = {
146
146
  duplicate: string;
147
147
  addToList: string;
148
148
  inAList: string;
149
+ /** Placeholder in the record search field. */
150
+ searchRecordsPlaceholder: string;
151
+ /** Placeholder in a select cell's option search. */
152
+ searchOptionsPlaceholder: string;
153
+ /** Prefix on the bulk bar's selected count. */
154
+ selectedCount: string;
149
155
  delete: string;
150
156
  sendEmail: string;
151
157
  exportCsv: string;
@@ -52,6 +52,8 @@ export type SidebarProps = React.ComponentPropsWithoutRef<"nav"> & {
52
52
  collapseLabel?: string;
53
53
  /** Label for the inbox quick-action. */
54
54
  inboxLabel?: string;
55
+ /** Label for the search / command-palette button. */
56
+ searchLabel?: string;
55
57
  asChild?: boolean;
56
58
  /** Called when the collapse-panel button in the header is pressed. */
57
59
  onToggleCollapse?: () => void;
@@ -85,6 +87,8 @@ export declare const Sidebar: React.ForwardRefExoticComponent<Omit<React.Detaile
85
87
  collapseLabel?: string;
86
88
  /** Label for the inbox quick-action. */
87
89
  inboxLabel?: string;
90
+ /** Label for the search / command-palette button. */
91
+ searchLabel?: string;
88
92
  asChild?: boolean;
89
93
  /** Called when the collapse-panel button in the header is pressed. */
90
94
  onToggleCollapse?: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",