@crisp-ui-kit/crisp 0.53.2 → 0.53.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/cjs/components/breadcrumb/breadcrumb.js +6 -1
  2. package/dist/cjs/components/columncalc/columncalc.js +13 -4
  3. package/dist/cjs/components/composewindow/composewindow.js +5 -1
  4. package/dist/cjs/components/contextmenu/contextmenu.js +3 -1
  5. package/dist/cjs/components/filldoc/filldoc.js +6 -1
  6. package/dist/cjs/components/heatmapchart/heatmapchart.js +24 -0
  7. package/dist/cjs/components/heatmapchart/heatmapchart.recipe.js +49 -0
  8. package/dist/cjs/components/heatmapchart/index.js +17 -0
  9. package/dist/cjs/components/spinner/spinner.js +4 -1
  10. package/dist/cjs/components/toast/toast.recipe.js +8 -4
  11. package/dist/cjs/core/i18n.js +17 -2
  12. package/dist/cjs/core/index.js +2 -1
  13. package/dist/cjs/index.js +2 -0
  14. package/dist/cjs/product/recordpage/recordpage.js +17 -2
  15. package/dist/cjs/product/recordstable/recordstable.js +14 -3
  16. package/dist/cjs/product/richtext/richtext.js +33 -4
  17. package/dist/cjs/product/statlist/index.js +17 -0
  18. package/dist/cjs/product/statlist/statlist.js +20 -0
  19. package/dist/cjs/product/statlist/statlist.recipe.js +32 -0
  20. package/dist/esm/components/breadcrumb/breadcrumb.js +6 -1
  21. package/dist/esm/components/columncalc/columncalc.js +13 -4
  22. package/dist/esm/components/composewindow/composewindow.js +6 -2
  23. package/dist/esm/components/contextmenu/contextmenu.js +4 -2
  24. package/dist/esm/components/filldoc/filldoc.js +7 -2
  25. package/dist/esm/components/heatmapchart/heatmapchart.js +21 -0
  26. package/dist/esm/components/heatmapchart/heatmapchart.recipe.js +46 -0
  27. package/dist/esm/components/heatmapchart/index.js +1 -0
  28. package/dist/esm/components/spinner/spinner.js +4 -1
  29. package/dist/esm/components/toast/toast.recipe.js +9 -5
  30. package/dist/esm/core/i18n.js +16 -2
  31. package/dist/esm/core/index.js +1 -1
  32. package/dist/esm/index.js +2 -0
  33. package/dist/esm/product/recordpage/recordpage.js +18 -3
  34. package/dist/esm/product/recordstable/recordstable.js +15 -4
  35. package/dist/esm/product/richtext/richtext.js +33 -4
  36. package/dist/esm/product/statlist/index.js +1 -0
  37. package/dist/esm/product/statlist/statlist.js +17 -0
  38. package/dist/esm/product/statlist/statlist.recipe.js +29 -0
  39. package/dist/heatmapchart.css +27 -0
  40. package/dist/heatmapchart.d.ts +7 -0
  41. package/dist/heatmapchart.layered.css +29 -0
  42. package/dist/statlist.css +8 -0
  43. package/dist/statlist.d.ts +7 -0
  44. package/dist/statlist.layered.css +10 -0
  45. package/dist/styles.css +39 -2
  46. package/dist/styles.layered.css +39 -2
  47. package/dist/toast.css +2 -2
  48. package/dist/toast.layered.css +2 -2
  49. package/dist/types/components/breadcrumb/breadcrumb.d.ts +1 -1
  50. package/dist/types/components/columncalc/columncalc.d.ts +1 -1
  51. package/dist/types/components/composewindow/composewindow.d.ts +1 -1
  52. package/dist/types/components/contextmenu/contextmenu.d.ts +1 -1
  53. package/dist/types/components/heatmapchart/heatmapchart.d.ts +51 -0
  54. package/dist/types/components/heatmapchart/heatmapchart.recipe.d.ts +3 -0
  55. package/dist/types/components/heatmapchart/index.d.ts +1 -0
  56. package/dist/types/components/spinner/spinner.d.ts +1 -1
  57. package/dist/types/core/i18n.d.ts +30 -1
  58. package/dist/types/core/index.d.ts +1 -1
  59. package/dist/types/index.d.ts +2 -0
  60. package/dist/types/product/statlist/index.d.ts +1 -0
  61. package/dist/types/product/statlist/statlist.d.ts +34 -0
  62. package/dist/types/product/statlist/statlist.recipe.d.ts +3 -0
  63. package/package.json +11 -1
@@ -36,12 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Breadcrumb = Breadcrumb;
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 breadcrumb_recipe_1 = require("./breadcrumb.recipe");
40
41
  /**
41
42
  * A breadcrumb trail — measured from a record-page nav. The last item is
42
43
  * marked `aria-current="page"`; earlier items render as links or buttons.
43
44
  */
44
- function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
45
+ function Breadcrumb({ items, separator, "aria-label": ariaLabelProp, className, ...rest }) {
46
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
47
+ // CrispMessagesProvider's translation of the English default, or that
48
+ // default itself with no provider — unchanged from before this existed.
49
+ const ariaLabel = (0, core_1.useCrispMessage)(ariaLabelProp, "Breadcrumb");
45
50
  // Live-measured (app.attio.com/<ws>/home, /companies,
46
51
  // /company/<id>/overview): a plain rounded divider, not a chevron -- a
47
52
  // symmetric shape needs no RTL mirroring, unlike the ChevronRight this
@@ -40,6 +40,7 @@ exports.ColumnCalc = ColumnCalc;
40
40
  const jsx_runtime_1 = require("react/jsx-runtime");
41
41
  const lucide_react_1 = require("lucide-react");
42
42
  const React = __importStar(require("react"));
43
+ const core_1 = require("../../core");
43
44
  const menu_1 = require("../menu");
44
45
  const columncalc_recipe_1 = require("./columncalc.recipe");
45
46
  /** The four calcs offered on every column, in their order. */
@@ -117,10 +118,18 @@ function computeCalc(values, op) {
117
118
  const defaultFormat = (n, op) => op === "percentEmpty" || op === "percentFilled"
118
119
  ? `${Math.round(n)}%`
119
120
  : n.toLocaleString(undefined, { maximumFractionDigits: 2 });
120
- function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel = "Add calculation", sectionLabel = "Aggregations", format = defaultFormat, align, opLabels, className, ...rest }) {
121
- // a partial override, so a consumer can translate one op without
122
- // restating all nine
123
- const labelFor = (o) => opLabels?.[o] ?? exports.CALC_LABEL[o];
121
+ function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel: addLabelProp, sectionLabel: sectionLabelProp, format = defaultFormat, align, opLabels, className, ...rest }) {
122
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
123
+ // CrispMessagesProvider's translation of the English default, or that
124
+ // default itself with no provider — unchanged from before this existed.
125
+ const addLabel = (0, core_1.useCrispMessage)(addLabelProp, "Add calculation");
126
+ const sectionLabel = (0, core_1.useCrispMessage)(sectionLabelProp, "Aggregations");
127
+ const crispMessages = (0, core_1.useCrispMessages)();
128
+ // opLabels (a per-op partial override) wins, then the provider's own
129
+ // translation of CALC_LABEL's English default, then that default itself —
130
+ // a consumer can translate one op via opLabels without restating all
131
+ // nine, AND get the rest from a provider with no per-instance prop at all.
132
+ const labelFor = (o) => opLabels?.[o] ?? crispMessages[exports.CALC_LABEL[o]] ?? exports.CALC_LABEL[o];
124
133
  const [internal, setInternal] = React.useState(defaultOp);
125
134
  const op = opProp ?? internal;
126
135
  const setOp = (next) => {
@@ -93,7 +93,11 @@ function useComposeWindowInstanceId() {
93
93
  * measured screenshot. `trapFocus` follows the same call — a picture-in-
94
94
  * picture window, not a modal, so Tab is not caught inside it.
95
95
  */
96
- function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel = "Minimize", pinLabel = "Pin to screen", closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
96
+ function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel: minimizeLabelProp, pinLabel: pinLabelProp, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
97
+ // crisp/#471: same resolution order as Breadcrumb's own.
98
+ const minimizeLabel = (0, core_1.useCrispMessage)(minimizeLabelProp, "Minimize");
99
+ const pinLabel = (0, core_1.useCrispMessage)(pinLabelProp, "Pin to screen");
100
+ const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
97
101
  const id = useComposeWindowInstanceId();
98
102
  const [internalMinimized, setInternalMinimized] = React.useState(defaultMinimized ?? false);
99
103
  const minimized = minimizedProp ?? internalMinimized;
@@ -45,7 +45,9 @@ const contextmenu_recipe_1 = require("./contextmenu.recipe");
45
45
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
46
46
  * edges), closing on select, outside pointer-down, and Escape.
47
47
  */
48
- function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
48
+ function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, }) {
49
+ // crisp/#471: same resolution order as Breadcrumb's own.
50
+ const menuLabel = (0, core_1.useCrispMessage)(menuLabelProp, "Context menu");
49
51
  const rootRef = React.useRef(null);
50
52
  const menuRef = React.useRef(null);
51
53
  const returnFocusTo = React.useRef(null);
@@ -72,7 +72,12 @@ const emptyTable = () => ({
72
72
  * page covered in handles and grips stops looking like a page, which is the
73
73
  * whole thing this is for.
74
74
  */
75
- exports.FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel = "Text", addHeadingLabel = "Heading", addTableLabel = "Table", removeLabel = "Remove", className, style, ...rest }, forwardedRef) {
75
+ exports.FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel: addTextLabelProp, addHeadingLabel: addHeadingLabelProp, addTableLabel: addTableLabelProp, removeLabel: removeLabelProp, className, style, ...rest }, forwardedRef) {
76
+ // crisp/#471: same resolution order as Breadcrumb's own.
77
+ const addTextLabel = (0, core_1.useCrispMessage)(addTextLabelProp, "Text");
78
+ const addHeadingLabel = (0, core_1.useCrispMessage)(addHeadingLabelProp, "Heading");
79
+ const addTableLabel = (0, core_1.useCrispMessage)(addTableLabelProp, "Table");
80
+ const removeLabel = (0, core_1.useCrispMessage)(removeLabelProp, "Remove");
76
81
  const editable = mode === "edit";
77
82
  const replace = (i, block) => {
78
83
  const next = blocks.slice();
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HeatmapChart = HeatmapChart;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const heatmapchart_recipe_1 = require("./heatmapchart.recipe");
6
+ /**
7
+ * An NxM cell-matrix chart — GRC's own risk heatmap (impact x likelihood)
8
+ * is the first consumer. Severity/intensity mapping (`tone`, `ring`) is
9
+ * entirely the caller's own data, not a scale this component invents; see
10
+ * the recipe's own doc comment for why (and for its measurement — or lack
11
+ * of one — against Attio).
12
+ */
13
+ function HeatmapChart({ data, rowAxis, colAxis, format = (cell) => (cell.value ? cell.value : ""), cellTitle, onCellClick, isCellClickable = (cell) => cell.value > 0, empty, emptyLabel = "Nothing to plot yet.", "aria-label": ariaLabel, className, ...rest }) {
14
+ const cols = data[0]?.length ?? 0;
15
+ const summary = ariaLabel ?? `Heatmap: ${data.length} x ${cols} grid`;
16
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, heatmapchart_recipe_1.heatmapchartRecipe)({ className }), ...rest, role: "img", "aria-label": summary, children: empty ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-heatmapchart-empty", children: emptyLabel })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-heatmapchart-rowaxis", children: [(0, jsx_runtime_1.jsx)("span", { children: rowAxis.high }), rowAxis.title != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-heatmapchart-rowaxis-title", children: rowAxis.title })), (0, jsx_runtime_1.jsx)("span", { children: rowAxis.low })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-heatmapchart-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-heatmapchart-grid", style: { gridTemplateColumns: `repeat(${cols}, 1fr)` }, children: data.map((row, r) => row.map((cell, cc) => {
17
+ const clickable = onCellClick != null && isCellClickable(cell, r, cc);
18
+ const title = cellTitle?.(cell, r, cc);
19
+ const key = `${r}-${cc}`;
20
+ const tone = cell.tone ?? "neutral";
21
+ return clickable ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-heatmapchart-cell", "data-tone": tone, "data-ring": cell.ring ? "" : undefined, title: title, onClick: () => onCellClick?.(r, cc, cell), children: format(cell, r, cc) }, key)) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-heatmapchart-cell", "data-tone": tone, "data-ring": cell.ring ? "" : undefined, title: title, children: format(cell, r, cc) }, key));
22
+ })) }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-heatmapchart-colaxis", children: [(0, jsx_runtime_1.jsx)("span", { children: colAxis.low }), colAxis.title != null && (0, jsx_runtime_1.jsx)("span", { children: colAxis.title }), (0, jsx_runtime_1.jsx)("span", { children: colAxis.high })] })] })] })) }));
23
+ }
24
+ HeatmapChart.displayName = "HeatmapChart";
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.heatmapchartRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ const soft = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, transparent)`;
7
+ // An NxM cell-matrix chart, alongside BarChart/LineChart/DonutChart — GRC's
8
+ // own risk heatmap (impact x likelihood) is the first consumer. No Attio
9
+ // widget in the workspace this was checked against (Reports/Dashboards:
10
+ // bar/line/number report tiles) has a matching matrix chart to measure a
11
+ // pixel value from, so this is composed entirely from already-measured
12
+ // crisp values: the same `color-mix` soft-tint recipe Badge's own `soft()`
13
+ // helper and StatCard's delta pill already use, at the same 14-16%,
14
+ // against the token contract's own success/warning/orange/danger roles.
15
+ // A cell's text stays `fgPrimary` at every tone — not tone-matched — the
16
+ // same "the background already carries the severity, tone-on-tone measures
17
+ // illegible" reasoning GRC's own hand-built heatmap already recorded.
18
+ const config = (0, core_1.defineRecipe)({
19
+ name: "heatmapchart",
20
+ base: `display: flex; gap: ${tokens_1.contract.space["2"]}; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans};
21
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
22
+
23
+ & .crisp-heatmapchart-rowaxis { display: flex; flex-direction: column; justify-content: space-between; padding: ${tokens_1.contract.space["1"]} 0; font-size: ${tokens_1.contract.text.xs}; font-weight: ${tokens_1.contract.weight.medium}; text-transform: uppercase; letter-spacing: 0.02em; color: ${tokens_1.contract.fgNeutralSubtle}; }
24
+ & .crisp-heatmapchart-rowaxis-title { writing-mode: vertical-rl; transform: rotate(180deg); align-self: center; }
25
+
26
+ & .crisp-heatmapchart-body { flex: 1; }
27
+ & .crisp-heatmapchart-grid { display: grid; gap: ${tokens_1.contract.space["1.5"]}; }
28
+
29
+ & .crisp-heatmapchart-cell { box-sizing: border-box; display: flex; align-items: center; justify-content: center; aspect-ratio: 2 / 1; border-radius: ${tokens_1.contract.radius.md}; border: none; background: transparent; margin: 0; padding: 0; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.semibold}; font-variant-numeric: tabular-nums; color: ${tokens_1.contract.fgPrimary}; }
30
+ & button.crisp-heatmapchart-cell { cursor: pointer; transition: filter 0.12s ease; }
31
+ & button.crisp-heatmapchart-cell:hover { filter: brightness(0.97); }
32
+ & button.crisp-heatmapchart-cell:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
33
+
34
+ & .crisp-heatmapchart-cell[data-tone="neutral"] { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgMuted}; }
35
+ & .crisp-heatmapchart-cell[data-tone="success"] { background: ${soft(tokens_1.contract.bgSuccess, 16)}; }
36
+ & .crisp-heatmapchart-cell[data-tone="warning"] { background: ${soft(tokens_1.contract.bgWarning, 16)}; }
37
+ & .crisp-heatmapchart-cell[data-tone="orange"] { background: ${soft(tokens_1.contract.bgOrange, 18)}; }
38
+ & .crisp-heatmapchart-cell[data-tone="danger"] { background: ${soft(tokens_1.contract.bgDanger, 16)}; }
39
+ & .crisp-heatmapchart-cell[data-ring] { box-shadow: inset 0 0 0 2px ${tokens_1.contract.bgDangerSolid}; }
40
+
41
+ & .crisp-heatmapchart-colaxis { display: flex; justify-content: space-between; margin-top: ${tokens_1.contract.space["1.5"]}; padding: 0 ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.xs}; font-weight: ${tokens_1.contract.weight.medium}; text-transform: uppercase; letter-spacing: 0.02em; color: ${tokens_1.contract.fgNeutralSubtle}; }
42
+
43
+ & .crisp-heatmapchart-empty { padding: ${tokens_1.contract.space["8"]} 0; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }`,
44
+ variants: {},
45
+ defaultVariants: {},
46
+ compoundVariants: [],
47
+ });
48
+ exports.heatmapchartRecipe = (0, core_1.resolveRecipe)(config);
49
+ exports.default = config;
@@ -0,0 +1,17 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./heatmapchart"), exports);
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Spinner = Spinner;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const core_1 = require("../../core");
5
6
  const spinner_recipe_1 = require("./spinner.recipe");
6
7
  /** A spinning ring indicator. Reports `role="status"` for assistive tech. */
7
- function Spinner({ size = 16, "aria-label": ariaLabel = "Loading", className, ...rest }) {
8
+ function Spinner({ size = 16, "aria-label": ariaLabelProp, className, ...rest }) {
9
+ // crisp/#471: same resolution order as Breadcrumb's own.
10
+ const ariaLabel = (0, core_1.useCrispMessage)(ariaLabelProp, "Loading");
8
11
  return ((0, jsx_runtime_1.jsx)("span", { role: "status", "aria-label": ariaLabel, className: (0, spinner_recipe_1.spinnerRecipe)({ className }), ...rest, style: {
9
12
  width: size,
10
13
  height: size,
@@ -3,14 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toastRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
- // Toast notifications — a fixed bottom-right stack of cards on the measured
6
+ // Toast notifications — a fixed TOP-right stack of cards on the measured
7
7
  // popover surface, each with a tone icon, title/description, optional action,
8
- // and a close button.
8
+ // and a close button. Position, radius, padding, surface and shadow measured
9
+ // live off Attio's own "Copied to clipboard!" toast (crisp#515): the stack
10
+ // anchors top-right (not bottom-right), and the card shares Menu/Popover's
11
+ // `bgPopover` surface + `elevation.floating` shadow (Lesson 31 — reference
12
+ // the shared token, never a hand-typed near-duplicate).
9
13
  const config = (0, core_1.defineRecipe)({
10
14
  name: "toast",
11
15
  base: `display: contents; font-family: ${tokens_1.contract.font.sans};
12
- & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: ${tokens_1.contract.space["4"]}; bottom: ${tokens_1.contract.space["4"]}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; width: 360px; max-width: calc(100vw - ${tokens_1.contract.space["8"]}); pointer-events: none; }
13
- & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: ${tokens_1.contract.space["2.5"]}; box-sizing: border-box; padding: ${tokens_1.contract.space["3"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgRaised}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10); opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
16
+ & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: ${tokens_1.contract.space["4"]}; top: ${tokens_1.contract.space["4"]}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; width: 360px; max-width: calc(100vw - ${tokens_1.contract.space["8"]}); pointer-events: none; }
17
+ & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: ${tokens_1.contract.space["2.5"]}; box-sizing: border-box; padding: ${tokens_1.contract.space["2.5"]}; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
14
18
  & .crisp-toast-icon { flex-shrink: 0; margin-top: 1px; display: inline-flex; color: ${tokens_1.contract.fgMuted}; }
15
19
  & .crisp-toast[data-tone="info"] .crisp-toast-icon { color: ${tokens_1.contract.bgBrandSolid}; }
16
20
  & .crisp-toast[data-tone="success"] .crisp-toast-icon { color: ${tokens_1.contract.bgSuccess}; }
@@ -36,11 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CrispMessagesProvider = CrispMessagesProvider;
37
37
  exports.useCrispMessage = useCrispMessage;
38
38
  exports.useCrispMessages = useCrispMessages;
39
+ exports.useCrispComponentDefaults = useCrispComponentDefaults;
39
40
  const jsx_runtime_1 = require("react/jsx-runtime");
40
41
  const React = __importStar(require("react"));
41
42
  const CrispMessagesContext = React.createContext({});
42
- function CrispMessagesProvider({ messages, children, }) {
43
- return ((0, jsx_runtime_1.jsx)(CrispMessagesContext.Provider, { value: messages, children: children }));
43
+ const CrispComponentDefaultsContext = React.createContext({});
44
+ function CrispMessagesProvider({ messages, componentDefaults, children, }) {
45
+ return ((0, jsx_runtime_1.jsx)(CrispMessagesContext.Provider, { value: messages, children: (0, jsx_runtime_1.jsx)(CrispComponentDefaultsContext.Provider, { value: componentDefaults ?? {}, children: children }) }));
44
46
  }
45
47
  /**
46
48
  * Resolves one translatable string for a component: `explicit` is whatever
@@ -73,3 +75,16 @@ function useCrispMessage(explicit, englishDefault) {
73
75
  function useCrispMessages() {
74
76
  return React.useContext(CrispMessagesContext);
75
77
  }
78
+ /**
79
+ * Reads one component's own whole-object defaults override, set via
80
+ * `CrispMessagesProvider`'s `componentDefaults` prop (see its own doc for
81
+ * why this exists separately from `useCrispMessage`). `T` is the
82
+ * component's own labels TYPE (e.g. `Partial<RecordPageLabels>`) — this
83
+ * hook itself only knows `name`, a plain string key; the caller supplies
84
+ * and trusts the real shape, the same way `useContext` always has for an
85
+ * untyped-at-the-provider value.
86
+ */
87
+ function useCrispComponentDefaults(name) {
88
+ const all = React.useContext(CrispComponentDefaultsContext);
89
+ return all[name];
90
+ }
@@ -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.usePrefersReducedMotion = exports.useMentionTrigger = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.matchMentionTrigger = exports.useCrispMessages = exports.useCrispMessage = exports.CrispMessagesProvider = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = exports.caretRectInField = void 0;
17
+ exports.usePrefersReducedMotion = exports.useMentionTrigger = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.matchMentionTrigger = exports.useCrispMessages = exports.useCrispMessage = exports.useCrispComponentDefaults = exports.CrispMessagesProvider = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = exports.caretRectInField = void 0;
18
18
  var caret_rect_1 = require("./caret-rect");
19
19
  Object.defineProperty(exports, "caretRectInField", { enumerable: true, get: function () { return caret_rect_1.caretRectInField; } });
20
20
  var cell_date_1 = require("./cell-date");
@@ -39,6 +39,7 @@ var form_value_1 = require("./form-value");
39
39
  Object.defineProperty(exports, "formValueStyle", { enumerable: true, get: function () { return form_value_1.formValueStyle; } });
40
40
  var i18n_1 = require("./i18n");
41
41
  Object.defineProperty(exports, "CrispMessagesProvider", { enumerable: true, get: function () { return i18n_1.CrispMessagesProvider; } });
42
+ Object.defineProperty(exports, "useCrispComponentDefaults", { enumerable: true, get: function () { return i18n_1.useCrispComponentDefaults; } });
42
43
  Object.defineProperty(exports, "useCrispMessage", { enumerable: true, get: function () { return i18n_1.useCrispMessage; } });
43
44
  Object.defineProperty(exports, "useCrispMessages", { enumerable: true, get: function () { return i18n_1.useCrispMessages; } });
44
45
  var mention_1 = require("./mention");
package/dist/cjs/index.js CHANGED
@@ -45,6 +45,7 @@ __exportStar(require("./components/fileupload"), exports);
45
45
  __exportStar(require("./components/filldoc"), exports);
46
46
  __exportStar(require("./components/fillsheet"), exports);
47
47
  __exportStar(require("./components/formfield"), exports);
48
+ __exportStar(require("./components/heatmapchart"), exports);
48
49
  __exportStar(require("./components/input"), exports);
49
50
  __exportStar(require("./components/kbd"), exports);
50
51
  __exportStar(require("./components/linechart"), exports);
@@ -94,6 +95,7 @@ __exportStar(require("./product/richtext"), exports);
94
95
  __exportStar(require("./product/settingssection"), exports);
95
96
  __exportStar(require("./product/settingsshell"), exports);
96
97
  __exportStar(require("./product/sidebar"), exports);
98
+ __exportStar(require("./product/statlist"), exports);
97
99
  __exportStar(require("./product/tasklist"), exports);
98
100
  __exportStar(require("./product/trialbanner"), exports);
99
101
  __exportStar(require("./product/viewtoggle"), exports);
@@ -257,19 +257,34 @@ function EmptyCard({ icon, children, }) {
257
257
  */
258
258
  function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, ...rest }) {
259
259
  const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
260
+ // crisp/#471: a CrispMessagesProvider's `componentDefaults.recordPage` sits
261
+ // BETWEEN the built-in English defaults and this instance's own explicit
262
+ // `labels` prop — a provider translates every RecordPage on the page at
263
+ // once, and a caller who still passes `labels` for one instance keeps
264
+ // overriding it there, same as before this existed. Not `useCrispMessage`:
265
+ // several of these entries (`domainSetTo`, `sortedBy`, …) are formatter
266
+ // FUNCTIONS, which a flat string dictionary can't hold.
267
+ const providerLabels = (0, core_1.useCrispComponentDefaults)("recordPage");
260
268
  const t = React.useMemo(() => ({
261
269
  ...exports.recordPageLabels,
270
+ ...providerLabels,
262
271
  ...labels,
263
- tabs: { ...exports.recordPageLabels.tabs, ...labels?.tabs },
272
+ tabs: {
273
+ ...exports.recordPageLabels.tabs,
274
+ ...providerLabels?.tabs,
275
+ ...labels?.tabs,
276
+ },
264
277
  highlightLabels: {
265
278
  ...exports.recordPageLabels.highlightLabels,
279
+ ...providerLabels?.highlightLabels,
266
280
  ...labels?.highlightLabels,
267
281
  },
268
282
  highlightEmpty: {
269
283
  ...exports.recordPageLabels.highlightEmpty,
284
+ ...providerLabels?.highlightEmpty,
270
285
  ...labels?.highlightEmpty,
271
286
  },
272
- }), [labels]);
287
+ }), [labels, providerLabels]);
273
288
  const [favorite, setFavorite] = React.useState(false);
274
289
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
275
290
  // `[]`) -- otherwise RecordPage keeps its own self-contained state, as
@@ -495,12 +495,23 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
495
495
  const [filterConds, setFilterConds] = React.useState([]);
496
496
  const [filterQuery, setFilterQuery] = React.useState("");
497
497
  // one popover open at a time: "sort" | "filter" | "more" | `sum:<key>`
498
+ // crisp/#471: same "provider sits between the built-in default and this
499
+ // instance's own explicit labels prop" order as RecordPage's own — see
500
+ // its comment. Several entries here (`selectionAnnouncement`, `sortedBy`,
501
+ // `recordCount`, …) are formatter functions, which is why this goes
502
+ // through `useCrispComponentDefaults`, not `useCrispMessage`.
503
+ const providerLabels = (0, core_1.useCrispComponentDefaults)("recordsTable");
498
504
  const t = React.useMemo(() => ({
499
505
  ...exports.recordsTableLabels,
506
+ ...providerLabels,
500
507
  ...labels,
501
- agg: { ...exports.recordsTableLabels.agg, ...labels?.agg },
502
- op: { ...exports.recordsTableLabels.op, ...labels?.op },
503
- }), [labels]);
508
+ agg: {
509
+ ...exports.recordsTableLabels.agg,
510
+ ...providerLabels?.agg,
511
+ ...labels?.agg,
512
+ },
513
+ op: { ...exports.recordsTableLabels.op, ...providerLabels?.op, ...labels?.op },
514
+ }), [labels, providerLabels]);
504
515
  const [openMenu, setOpenMenu] = React.useState(null);
505
516
  // the element that opened the current menu, so Esc/select can hand focus back
506
517
  const menuReturn = React.useRef(null);
@@ -170,6 +170,19 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
170
170
  // entirely", the same distinction `updateBar` already makes for the block
171
171
  // menu's search input below.
172
172
  const mentionPickerRef = React.useRef(null);
173
+ // Whether focus currently sits somewhere inside whatever
174
+ // `renderMentionPicker` returned (#514) — tracked through React's own
175
+ // onFocus/onBlur, not `mentionPickerRef.current.contains(activeElement)`.
176
+ // A picker built from crisp's own `Command` (the shape #372's own doc
177
+ // recommends) renders through a `Portal`, so its DOM parent is nowhere
178
+ // near `mentionPickerRef` even though it's still that ref's REACT child;
179
+ // `.contains()` sees a stranger the instant the picker's search input
180
+ // takes focus and updateMention closes the picker it was just asked to
181
+ // protect. React's synthetic focus/blur events bubble through the
182
+ // component tree a portal renders into, not the DOM tree it renders to
183
+ // (documented React behaviour), so a plain onFocus/onBlur pair on this
184
+ // same wrapper sees the portal's contents correctly.
185
+ const mentionPickerHasFocusRef = React.useRef(false);
173
186
  const syncEmpty = (el) => {
174
187
  if (el)
175
188
  el.toggleAttribute("data-empty", (el.textContent ?? "") === "");
@@ -291,6 +304,11 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
291
304
  }, [readOnly, blockMenu, closeBlockMenu]);
292
305
  const closeMention = React.useCallback(() => {
293
306
  mentionRangeRef.current = null;
307
+ // Defensive, not load-bearing: the wrapper's onBlur already clears this
308
+ // when the picker's own focused element leaves the DOM on unmount, but
309
+ // resetting it directly here means a stale `true` can never survive
310
+ // past whatever actually closed the picker, blur-ordering included.
311
+ mentionPickerHasFocusRef.current = false;
294
312
  setMention(null);
295
313
  }, []);
296
314
  /**
@@ -302,16 +320,23 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
302
320
  * no trigger.
303
321
  *
304
322
  * Does NOT auto-close while focus sits inside the consumer's own picker
305
- * (`mentionPickerRef`) — a picker with its own search field would
323
+ * (`mentionPickerHasFocusRef`) — a picker with its own search field would
306
324
  * otherwise close itself the instant that field took focus, since focus
307
325
  * moving there also moves the document selection out of `body`. This is
308
326
  * the same distinction `updateBar` already draws for the block menu's own
309
- * search input, made while merging the two features together.
327
+ * search input, made while merging the two features together — except a
328
+ * consumer's picker, unlike the block menu's own inline search, is free
329
+ * to render through a Portal (crisp's own `Command`, the shape #372's own
330
+ * doc recommends building one from, does exactly that), so this reads a
331
+ * ref React's focus/blur bubbling keeps current instead of asking
332
+ * `mentionPickerRef`'s DOM node whether it contains `activeElement` (#514
333
+ * — a Portal's content is a DOM stranger to its React parent, so that
334
+ * check silently stopped protecting the one case it exists for).
310
335
  */
311
336
  const updateMention = React.useCallback(() => {
312
337
  if (readOnly)
313
338
  return;
314
- if (mentionPickerRef.current?.contains(document.activeElement))
339
+ if (mentionPickerHasFocusRef.current)
315
340
  return;
316
341
  const body = bodyRef.current;
317
342
  const sel = window.getSelection();
@@ -564,7 +589,11 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
564
589
  select: insertMention,
565
590
  close: closeMention,
566
591
  });
567
- return rendered == null ? null : ((0, jsx_runtime_1.jsx)("div", { ref: mentionPickerRef, style: { display: "contents" }, children: rendered }));
592
+ return rendered == null ? null : ((0, jsx_runtime_1.jsx)("div", { ref: mentionPickerRef, style: { display: "contents" }, onFocus: () => {
593
+ mentionPickerHasFocusRef.current = true;
594
+ }, onBlur: () => {
595
+ mentionPickerHasFocusRef.current = false;
596
+ }, children: rendered }));
568
597
  })()] }));
569
598
  });
570
599
  exports.RichTextEditor.displayName = "RichTextEditor";
@@ -0,0 +1,17 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./statlist"), exports);
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StatList = StatList;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const badge_1 = require("../../components/badge/badge");
6
+ const statlist_recipe_1 = require("./statlist.recipe");
7
+ /**
8
+ * A list of label + trailing-count rows — a dashboard's own findings
9
+ * summary ("Open risks with no owner: 3") or an audit-readiness check list,
10
+ * each row optionally a link. Not measured against one specific Attio
11
+ * widget (see the recipe's own doc comment for why); composed from
12
+ * already-measured crisp values throughout.
13
+ */
14
+ function StatList({ items, emptyLabel = "Nothing to show", className, ...rest }) {
15
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, statlist_recipe_1.statlistRecipe)({ className }), ...rest, children: items.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-statlist-empty", children: emptyLabel })) : (items.map((item) => {
16
+ const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-statlist-label", children: item.label }), (0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: item.count === 0 ? "neutral" : (item.tone ?? "neutral"), children: item.count })] }));
17
+ return item.href ? ((0, jsx_runtime_1.jsx)("a", { href: item.href, onClick: item.onClick, className: "crisp-statlist-row", children: content }, item.id)) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-statlist-row", children: content }, item.id));
18
+ })) }));
19
+ }
20
+ StatList.displayName = "StatList";
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.statlistRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A list of label + trailing-count rows, each optionally a link — the
7
+ // "N findings of this kind" shape a dashboard's own status summary and an
8
+ // audit-readiness check list both need. No direct Attio widget measures
9
+ // this exact row (Attio's own Reports/Dashboards surface in this workspace
10
+ // has bar/line/number report tiles, not a findings list), so this reuses
11
+ // already-measured crisp values throughout rather than a fresh guess:
12
+ // TaskList's own row-divider structure (border-top, no border on the first
13
+ // row), the type scale's own `md`/`sm` sizes, and Badge's own tone colours
14
+ // for the trailing count — no new pixel or colour value is introduced here.
15
+ const config = (0, core_1.defineRecipe)({
16
+ name: "statlist",
17
+ base: `display: flex; flex-direction: column; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans};
18
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
19
+
20
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["3"]}; padding: ${tokens_1.contract.space["2.5"]} 0; border-top: 1px solid ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.md}; color: inherit; text-decoration: none; }
21
+ & .crisp-statlist-row:first-child { border-top: none; }
22
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: ${tokens_1.contract.fgBrandSoft}; }
23
+
24
+ & .crisp-statlist-label { color: ${tokens_1.contract.fgMuted}; }
25
+
26
+ & .crisp-statlist-empty { padding: ${tokens_1.contract.space["4"]} 0; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }`,
27
+ variants: {},
28
+ defaultVariants: {},
29
+ compoundVariants: [],
30
+ });
31
+ exports.statlistRecipe = (0, core_1.resolveRecipe)(config);
32
+ exports.default = config;
@@ -1,11 +1,16 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { useCrispMessage } from "../../core/index.js";
3
4
  import { breadcrumbRecipe } from "./breadcrumb.recipe.js";
4
5
  /**
5
6
  * A breadcrumb trail — measured from a record-page nav. The last item is
6
7
  * marked `aria-current="page"`; earlier items render as links or buttons.
7
8
  */
8
- export function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
9
+ export function Breadcrumb({ items, separator, "aria-label": ariaLabelProp, className, ...rest }) {
10
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
11
+ // CrispMessagesProvider's translation of the English default, or that
12
+ // default itself with no provider — unchanged from before this existed.
13
+ const ariaLabel = useCrispMessage(ariaLabelProp, "Breadcrumb");
9
14
  // Live-measured (app.attio.com/<ws>/home, /companies,
10
15
  // /company/<id>/overview): a plain rounded divider, not a chevron -- a
11
16
  // symmetric shape needs no RTL mirroring, unlike the ChevronRight this
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDown, Plus } from "lucide-react";
3
3
  import * as React from "react";
4
+ import { useCrispMessage, useCrispMessages } from "../../core/index.js";
4
5
  import { Menu, MenuItem } from "../menu/index.js";
5
6
  import { columncalcRecipe } from "./columncalc.recipe.js";
6
7
  /** The four calcs offered on every column, in their order. */
@@ -78,10 +79,18 @@ export function computeCalc(values, op) {
78
79
  const defaultFormat = (n, op) => op === "percentEmpty" || op === "percentFilled"
79
80
  ? `${Math.round(n)}%`
80
81
  : n.toLocaleString(undefined, { maximumFractionDigits: 2 });
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
- // a partial override, so a consumer can translate one op without
83
- // restating all nine
84
- const labelFor = (o) => opLabels?.[o] ?? CALC_LABEL[o];
82
+ export function ColumnCalc({ rows, value, numeric = false, defaultOp = "none", op: opProp, onOpChange, addLabel: addLabelProp, sectionLabel: sectionLabelProp, format = defaultFormat, align, opLabels, className, ...rest }) {
83
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
84
+ // CrispMessagesProvider's translation of the English default, or that
85
+ // default itself with no provider — unchanged from before this existed.
86
+ const addLabel = useCrispMessage(addLabelProp, "Add calculation");
87
+ const sectionLabel = useCrispMessage(sectionLabelProp, "Aggregations");
88
+ const crispMessages = useCrispMessages();
89
+ // opLabels (a per-op partial override) wins, then the provider's own
90
+ // translation of CALC_LABEL's English default, then that default itself —
91
+ // a consumer can translate one op via opLabels without restating all
92
+ // nine, AND get the rest from a provider with no per-instance prop at all.
93
+ const labelFor = (o) => opLabels?.[o] ?? crispMessages[CALC_LABEL[o]] ?? CALC_LABEL[o];
85
94
  const [internal, setInternal] = React.useState(defaultOp);
86
95
  const op = opProp ?? internal;
87
96
  const setOp = (next) => {
@@ -3,7 +3,7 @@ import * as dialog from "@zag-js/dialog";
3
3
  import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
4
  import { Minus, Pin, X } from "lucide-react";
5
5
  import * as React from "react";
6
- import { cx, withDescribedBy } from "../../core/index.js";
6
+ import { cx, useCrispMessage, withDescribedBy } from "../../core/index.js";
7
7
  import { composewindowRecipe } from "./composewindow.recipe.js";
8
8
  // A globally unique id per mounted instance — see command.tsx/dialog.tsx for
9
9
  // the rationale (the machine defers its dismissal listeners a frame, so a
@@ -56,7 +56,11 @@ function useComposeWindowInstanceId() {
56
56
  * measured screenshot. `trapFocus` follows the same call — a picture-in-
57
57
  * picture window, not a modal, so Tab is not caught inside it.
58
58
  */
59
- export function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel = "Minimize", pinLabel = "Pin to screen", closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
59
+ export function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel: minimizeLabelProp, pinLabel: pinLabelProp, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
60
+ // crisp/#471: same resolution order as Breadcrumb's own.
61
+ const minimizeLabel = useCrispMessage(minimizeLabelProp, "Minimize");
62
+ const pinLabel = useCrispMessage(pinLabelProp, "Pin to screen");
63
+ const closeLabel = useCrispMessage(closeLabelProp, "Close");
60
64
  const id = useComposeWindowInstanceId();
61
65
  const [internalMinimized, setInternalMinimized] = React.useState(defaultMinimized ?? false);
62
66
  const minimized = minimizedProp ?? internalMinimized;
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import * as React from "react";
4
- import { FloatingPortal, isInsideLaterFloatingLayer, registerFloatingLayer, } from "../../core/index.js";
4
+ import { FloatingPortal, isInsideLaterFloatingLayer, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
5
5
  import { useMenuKeyboard } from "../menu/index.js";
6
6
  import { contextmenuRecipe } from "./contextmenu.recipe.js";
7
7
  /**
@@ -9,7 +9,9 @@ import { contextmenuRecipe } from "./contextmenu.recipe.js";
9
9
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
10
10
  * edges), closing on select, outside pointer-down, and Escape.
11
11
  */
12
- export function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
12
+ export function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, }) {
13
+ // crisp/#471: same resolution order as Breadcrumb's own.
14
+ const menuLabel = useCrispMessage(menuLabelProp, "Context menu");
13
15
  const rootRef = React.useRef(null);
14
16
  const menuRef = React.useRef(null);
15
17
  const returnFocusTo = React.useRef(null);