@crisp-ui-kit/crisp 0.23.5 → 0.24.0

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.
@@ -44,8 +44,8 @@ const bulkbar_recipe_1 = require("./bulkbar.recipe");
44
44
  * positioned ancestor when a selection is active; render it conditionally
45
45
  * (e.g. `count > 0 && <BulkBar …/>`). Measured from Attio's records table.
46
46
  */
47
- function BulkBar({ count, onClear, countLabel = "selected", children, className, clearLabel = "Clear selection", ...rest }) {
48
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "bulkbar-bar", role: "status", children: [count != null && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "bulkbar-count", onClick: onClear, title: clearLabel, children: [count, " ", countLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
47
+ function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
48
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "bulkbar-bar", children: [count != null && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
49
49
  }
50
50
  BulkBar.displayName = "BulkBar";
51
51
  /** A styled action button for use inside a `<BulkBar>` (or as a `<Menu>` trigger). */
@@ -130,6 +130,7 @@ exports.recordsTableLabels = {
130
130
  searchRecordsPlaceholder: "Search records…",
131
131
  searchOptionsPlaceholder: "Search options…",
132
132
  selectedCount: "Selected",
133
+ selectionAnnouncement: (count) => count === 0 ? "Selection cleared" : `${count} selected`,
133
134
  delete: "Delete",
134
135
  sendEmail: "Send email",
135
136
  exportCsv: "Export CSV",
@@ -562,6 +563,18 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
562
563
  // selection — controlled if `selectedIds` is provided, else internal
563
564
  const [internalSel, setInternalSel] = React.useState(() => new Set());
564
565
  const selected = React.useMemo(() => (selectedIds ? new Set(selectedIds) : internalSel), [selectedIds, internalSel]);
566
+ // The selection is announced from a region that is part of the table and
567
+ // outlives every selection, because the bulk bar the consumer mounts on the
568
+ // first pick cannot announce its own arrival (#234). Derived by adjusting
569
+ // state during render rather than in an effect: it must not fire on mount —
570
+ // nobody has selected anything yet — and it has to cover a `selectedIds`
571
+ // change the consumer makes as well as a click in the table.
572
+ const prevSelectedCount = React.useRef(selected.size);
573
+ const [selectionAnnouncement, setSelectionAnnouncement] = React.useState("");
574
+ if (prevSelectedCount.current !== selected.size) {
575
+ prevSelectedCount.current = selected.size;
576
+ setSelectionAnnouncement(t.selectionAnnouncement(selected.size));
577
+ }
565
578
  const setSelected = React.useCallback((next) => {
566
579
  onSelectionChange?.([...next]);
567
580
  if (!selectedIds)
@@ -1094,7 +1107,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1094
1107
  (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));
1095
1108
  }), (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
1096
1109
  ? "None of your records match the filters that are set."
1097
- : `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 &&
1110
+ : `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] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), 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 &&
1098
1111
  (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)] }));
1099
1112
  }
1100
1113
  exports.defaultColumns = [
@@ -149,6 +149,12 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("recordstable")}
149
149
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
150
150
  & .recordstable-bulk-close:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
151
151
 
152
+ /* announced, never drawn */
153
+ & .recordstable-live {
154
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
155
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
156
+ }
157
+
152
158
  ${core_1.rtlFlipCss}
153
159
  `,
154
160
  variants: {},
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { X } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { bulkbarRecipe } from "./bulkbar.recipe.js";
@@ -7,8 +7,8 @@ import { bulkbarRecipe } from "./bulkbar.recipe.js";
7
7
  * positioned ancestor when a selection is active; render it conditionally
8
8
  * (e.g. `count > 0 && <BulkBar …/>`). Measured from Attio's records table.
9
9
  */
10
- export function BulkBar({ count, onClear, countLabel = "selected", children, className, clearLabel = "Clear selection", ...rest }) {
11
- return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "bulkbar-bar", role: "status", children: [count != null && (_jsxs("button", { type: "button", className: "bulkbar-count", onClick: onClear, title: clearLabel, children: [count, " ", countLabel, _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
10
+ export function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
11
+ return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
12
12
  }
13
13
  BulkBar.displayName = "BulkBar";
14
14
  /** A styled action button for use inside a `<BulkBar>` (or as a `<Menu>` trigger). */
@@ -90,6 +90,7 @@ export const recordsTableLabels = {
90
90
  searchRecordsPlaceholder: "Search records…",
91
91
  searchOptionsPlaceholder: "Search options…",
92
92
  selectedCount: "Selected",
93
+ selectionAnnouncement: (count) => count === 0 ? "Selection cleared" : `${count} selected`,
93
94
  delete: "Delete",
94
95
  sendEmail: "Send email",
95
96
  exportCsv: "Export CSV",
@@ -522,6 +523,18 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
522
523
  // selection — controlled if `selectedIds` is provided, else internal
523
524
  const [internalSel, setInternalSel] = React.useState(() => new Set());
524
525
  const selected = React.useMemo(() => (selectedIds ? new Set(selectedIds) : internalSel), [selectedIds, internalSel]);
526
+ // The selection is announced from a region that is part of the table and
527
+ // outlives every selection, because the bulk bar the consumer mounts on the
528
+ // first pick cannot announce its own arrival (#234). Derived by adjusting
529
+ // state during render rather than in an effect: it must not fire on mount —
530
+ // nobody has selected anything yet — and it has to cover a `selectedIds`
531
+ // change the consumer makes as well as a click in the table.
532
+ const prevSelectedCount = React.useRef(selected.size);
533
+ const [selectionAnnouncement, setSelectionAnnouncement] = React.useState("");
534
+ if (prevSelectedCount.current !== selected.size) {
535
+ prevSelectedCount.current = selected.size;
536
+ setSelectionAnnouncement(t.selectionAnnouncement(selected.size));
537
+ }
525
538
  const setSelected = React.useCallback((next) => {
526
539
  onSelectionChange?.([...next]);
527
540
  if (!selectedIds)
@@ -1054,7 +1067,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1054
1067
  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));
1055
1068
  }), _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
1056
1069
  ? "None of your records match the filters that are set."
1057
- : `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 &&
1070
+ : `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] }))] }), _jsx("div", { className: "recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), 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 &&
1058
1071
  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)] }));
1059
1072
  }
1060
1073
  export const defaultColumns = [
@@ -146,6 +146,12 @@ ${toolbarChromeBody("recordstable")}
146
146
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgMuted}; cursor: pointer; }
147
147
  & .recordstable-bulk-close:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
148
148
 
149
+ /* announced, never drawn */
150
+ & .recordstable-live {
151
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
152
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
153
+ }
154
+
149
155
  ${rtlFlipCss}
150
156
  `,
151
157
  variants: {},
@@ -201,6 +201,12 @@
201
201
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: var(--radius-md); background: transparent; color: var(--fg-muted); cursor: pointer; }
202
202
  & .recordstable-bulk-close:hover { background: var(--bg-raised); color: var(--fg-primary); }
203
203
 
204
+ /* announced, never drawn */
205
+ & .recordstable-live {
206
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
207
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
208
+ }
209
+
204
210
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
205
211
 
206
212
  }
package/dist/styles.css CHANGED
@@ -2603,6 +2603,12 @@
2603
2603
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: var(--radius-md); background: transparent; color: var(--fg-muted); cursor: pointer; }
2604
2604
  & .recordstable-bulk-close:hover { background: var(--bg-raised); color: var(--fg-primary); }
2605
2605
 
2606
+ /* announced, never drawn */
2607
+ & .recordstable-live {
2608
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
2609
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
2610
+ }
2611
+
2606
2612
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
2607
2613
 
2608
2614
  }
@@ -6,6 +6,14 @@ export type BulkBarProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
6
6
  onClear?: () => void;
7
7
  /** Word after the count in the pill (default "selected"). */
8
8
  countLabel?: string;
9
+ /**
10
+ * The whole pill line, given the count and `countLabel`. A formatter,
11
+ * because "3 selected" is a sentence with a hole in it and the count does
12
+ * not sit in the same place in every language. Defaults to
13
+ * `{count} {countLabel}`, so a consumer who only wants the word keeps
14
+ * passing `countLabel`.
15
+ */
16
+ countText?: (count: number, countLabel: string) => React.ReactNode;
9
17
  /** title on the count pill's clear (✕) control (default "Clear selection"). */
10
18
  clearLabel?: string;
11
19
  /** Action buttons — usually `<BulkBarButton>`s and a `<Menu>` overflow. */
@@ -17,7 +25,7 @@ export type BulkBarProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
17
25
  * positioned ancestor when a selection is active; render it conditionally
18
26
  * (e.g. `count > 0 && <BulkBar …/>`). Measured from Attio's records table.
19
27
  */
20
- export declare function BulkBar({ count, onClear, countLabel, children, className, clearLabel, ...rest }: BulkBarProps): React.JSX.Element;
28
+ export declare function BulkBar({ count, onClear, countLabel, countText, children, className, clearLabel, ...rest }: BulkBarProps): React.JSX.Element;
21
29
  export declare namespace BulkBar {
22
30
  var displayName: string;
23
31
  }
@@ -152,6 +152,14 @@ export type RecordsTableLabels = {
152
152
  searchOptionsPlaceholder: string;
153
153
  /** Prefix on the bulk bar's selected count. */
154
154
  selectedCount: string;
155
+ /**
156
+ * Announced when the selection changes. A formatter, not a string: the
157
+ * count moves within the sentence between languages. Return "" to say
158
+ * nothing. The bulk bar cannot carry this itself — the consumer mounts it
159
+ * when the first row is picked, so its live region would be created by the
160
+ * very change it should announce (#234).
161
+ */
162
+ selectionAnnouncement: (count: number) => string;
155
163
  delete: string;
156
164
  sendEmail: string;
157
165
  exportCsv: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.23.5",
3
+ "version": "0.24.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",