@crisp-ui-kit/crisp 0.53.3 → 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 (34) hide show
  1. package/dist/cjs/components/heatmapchart/heatmapchart.js +24 -0
  2. package/dist/cjs/components/heatmapchart/heatmapchart.recipe.js +49 -0
  3. package/dist/cjs/components/heatmapchart/index.js +17 -0
  4. package/dist/cjs/components/toast/toast.recipe.js +8 -4
  5. package/dist/cjs/core/i18n.js +17 -2
  6. package/dist/cjs/core/index.js +2 -1
  7. package/dist/cjs/index.js +1 -0
  8. package/dist/cjs/product/recordpage/recordpage.js +17 -2
  9. package/dist/cjs/product/recordstable/recordstable.js +14 -3
  10. package/dist/cjs/product/richtext/richtext.js +33 -4
  11. package/dist/esm/components/heatmapchart/heatmapchart.js +21 -0
  12. package/dist/esm/components/heatmapchart/heatmapchart.recipe.js +46 -0
  13. package/dist/esm/components/heatmapchart/index.js +1 -0
  14. package/dist/esm/components/toast/toast.recipe.js +9 -5
  15. package/dist/esm/core/i18n.js +16 -2
  16. package/dist/esm/core/index.js +1 -1
  17. package/dist/esm/index.js +1 -0
  18. package/dist/esm/product/recordpage/recordpage.js +18 -3
  19. package/dist/esm/product/recordstable/recordstable.js +15 -4
  20. package/dist/esm/product/richtext/richtext.js +33 -4
  21. package/dist/heatmapchart.css +27 -0
  22. package/dist/heatmapchart.d.ts +7 -0
  23. package/dist/heatmapchart.layered.css +29 -0
  24. package/dist/styles.css +30 -2
  25. package/dist/styles.layered.css +30 -2
  26. package/dist/toast.css +2 -2
  27. package/dist/toast.layered.css +2 -2
  28. package/dist/types/components/heatmapchart/heatmapchart.d.ts +51 -0
  29. package/dist/types/components/heatmapchart/heatmapchart.recipe.d.ts +3 -0
  30. package/dist/types/components/heatmapchart/index.d.ts +1 -0
  31. package/dist/types/core/i18n.d.ts +30 -1
  32. package/dist/types/core/index.d.ts +1 -1
  33. package/dist/types/index.d.ts +1 -0
  34. package/package.json +6 -1
@@ -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);
@@ -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);
@@ -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,21 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { heatmapchartRecipe } from "./heatmapchart.recipe.js";
3
+ /**
4
+ * An NxM cell-matrix chart — GRC's own risk heatmap (impact x likelihood)
5
+ * is the first consumer. Severity/intensity mapping (`tone`, `ring`) is
6
+ * entirely the caller's own data, not a scale this component invents; see
7
+ * the recipe's own doc comment for why (and for its measurement — or lack
8
+ * of one — against Attio).
9
+ */
10
+ export 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 }) {
11
+ const cols = data[0]?.length ?? 0;
12
+ const summary = ariaLabel ?? `Heatmap: ${data.length} x ${cols} grid`;
13
+ return (_jsx("div", { className: heatmapchartRecipe({ className }), ...rest, role: "img", "aria-label": summary, children: empty ? (_jsx("div", { className: "crisp-heatmapchart-empty", children: emptyLabel })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "crisp-heatmapchart-rowaxis", children: [_jsx("span", { children: rowAxis.high }), rowAxis.title != null && (_jsx("span", { className: "crisp-heatmapchart-rowaxis-title", children: rowAxis.title })), _jsx("span", { children: rowAxis.low })] }), _jsxs("div", { className: "crisp-heatmapchart-body", children: [_jsx("div", { className: "crisp-heatmapchart-grid", style: { gridTemplateColumns: `repeat(${cols}, 1fr)` }, children: data.map((row, r) => row.map((cell, cc) => {
14
+ const clickable = onCellClick != null && isCellClickable(cell, r, cc);
15
+ const title = cellTitle?.(cell, r, cc);
16
+ const key = `${r}-${cc}`;
17
+ const tone = cell.tone ?? "neutral";
18
+ return clickable ? (_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)) : (_jsx("div", { className: "crisp-heatmapchart-cell", "data-tone": tone, "data-ring": cell.ring ? "" : undefined, title: title, children: format(cell, r, cc) }, key));
19
+ })) }), _jsxs("div", { className: "crisp-heatmapchart-colaxis", children: [_jsx("span", { children: colAxis.low }), colAxis.title != null && _jsx("span", { children: colAxis.title }), _jsx("span", { children: colAxis.high })] })] })] })) }));
20
+ }
21
+ HeatmapChart.displayName = "HeatmapChart";
@@ -0,0 +1,46 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ const soft = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, transparent)`;
4
+ // An NxM cell-matrix chart, alongside BarChart/LineChart/DonutChart — GRC's
5
+ // own risk heatmap (impact x likelihood) is the first consumer. No Attio
6
+ // widget in the workspace this was checked against (Reports/Dashboards:
7
+ // bar/line/number report tiles) has a matching matrix chart to measure a
8
+ // pixel value from, so this is composed entirely from already-measured
9
+ // crisp values: the same `color-mix` soft-tint recipe Badge's own `soft()`
10
+ // helper and StatCard's delta pill already use, at the same 14-16%,
11
+ // against the token contract's own success/warning/orange/danger roles.
12
+ // A cell's text stays `fgPrimary` at every tone — not tone-matched — the
13
+ // same "the background already carries the severity, tone-on-tone measures
14
+ // illegible" reasoning GRC's own hand-built heatmap already recorded.
15
+ const config = defineRecipe({
16
+ name: "heatmapchart",
17
+ base: `display: flex; gap: ${c.space["2"]}; box-sizing: border-box; font-family: ${c.font.sans};
18
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
19
+
20
+ & .crisp-heatmapchart-rowaxis { display: flex; flex-direction: column; justify-content: space-between; padding: ${c.space["1"]} 0; font-size: ${c.text.xs}; font-weight: ${c.weight.medium}; text-transform: uppercase; letter-spacing: 0.02em; color: ${c.fgNeutralSubtle}; }
21
+ & .crisp-heatmapchart-rowaxis-title { writing-mode: vertical-rl; transform: rotate(180deg); align-self: center; }
22
+
23
+ & .crisp-heatmapchart-body { flex: 1; }
24
+ & .crisp-heatmapchart-grid { display: grid; gap: ${c.space["1.5"]}; }
25
+
26
+ & .crisp-heatmapchart-cell { box-sizing: border-box; display: flex; align-items: center; justify-content: center; aspect-ratio: 2 / 1; border-radius: ${c.radius.md}; border: none; background: transparent; margin: 0; padding: 0; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.semibold}; font-variant-numeric: tabular-nums; color: ${c.fgPrimary}; }
27
+ & button.crisp-heatmapchart-cell { cursor: pointer; transition: filter 0.12s ease; }
28
+ & button.crisp-heatmapchart-cell:hover { filter: brightness(0.97); }
29
+ & button.crisp-heatmapchart-cell:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
30
+
31
+ & .crisp-heatmapchart-cell[data-tone="neutral"] { background: ${c.bgRaised}; color: ${c.fgMuted}; }
32
+ & .crisp-heatmapchart-cell[data-tone="success"] { background: ${soft(c.bgSuccess, 16)}; }
33
+ & .crisp-heatmapchart-cell[data-tone="warning"] { background: ${soft(c.bgWarning, 16)}; }
34
+ & .crisp-heatmapchart-cell[data-tone="orange"] { background: ${soft(c.bgOrange, 18)}; }
35
+ & .crisp-heatmapchart-cell[data-tone="danger"] { background: ${soft(c.bgDanger, 16)}; }
36
+ & .crisp-heatmapchart-cell[data-ring] { box-shadow: inset 0 0 0 2px ${c.bgDangerSolid}; }
37
+
38
+ & .crisp-heatmapchart-colaxis { display: flex; justify-content: space-between; margin-top: ${c.space["1.5"]}; padding: 0 ${c.space["0.5"]}; font-size: ${c.text.xs}; font-weight: ${c.weight.medium}; text-transform: uppercase; letter-spacing: 0.02em; color: ${c.fgNeutralSubtle}; }
39
+
40
+ & .crisp-heatmapchart-empty { padding: ${c.space["8"]} 0; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }`,
41
+ variants: {},
42
+ defaultVariants: {},
43
+ compoundVariants: [],
44
+ });
45
+ export const heatmapchartRecipe = resolveRecipe(config);
46
+ export default config;
@@ -0,0 +1 @@
1
+ export * from "./heatmapchart.js";
@@ -1,13 +1,17 @@
1
1
  import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
- import { contract as c } from "../../tokens/index.js";
3
- // Toast notifications — a fixed bottom-right stack of cards on the measured
2
+ import { contract as c, elevation as e } from "../../tokens/index.js";
3
+ // Toast notifications — a fixed TOP-right stack of cards on the measured
4
4
  // popover surface, each with a tone icon, title/description, optional action,
5
- // and a close button.
5
+ // and a close button. Position, radius, padding, surface and shadow measured
6
+ // live off Attio's own "Copied to clipboard!" toast (crisp#515): the stack
7
+ // anchors top-right (not bottom-right), and the card shares Menu/Popover's
8
+ // `bgPopover` surface + `elevation.floating` shadow (Lesson 31 — reference
9
+ // the shared token, never a hand-typed near-duplicate).
6
10
  const config = defineRecipe({
7
11
  name: "toast",
8
12
  base: `display: contents; font-family: ${c.font.sans};
9
- & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: ${c.space["4"]}; bottom: ${c.space["4"]}; display: flex; flex-direction: column; gap: ${c.space["2"]}; width: 360px; max-width: calc(100vw - ${c.space["8"]}); pointer-events: none; }
10
- & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: ${c.space["2.5"]}; box-sizing: border-box; padding: ${c.space["3"]}; border-radius: ${c.radius.xl}; background: ${c.bgRaised}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.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); } }
13
+ & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: ${c.space["4"]}; top: ${c.space["4"]}; display: flex; flex-direction: column; gap: ${c.space["2"]}; width: 360px; max-width: calc(100vw - ${c.space["8"]}); pointer-events: none; }
14
+ & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: ${c.space["2.5"]}; box-sizing: border-box; padding: ${c.space["2.5"]}; border-radius: ${c.radius.md}; background: ${c.bgPopover}; box-shadow: ${e.floating}; opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
11
15
  & .crisp-toast-icon { flex-shrink: 0; margin-top: 1px; display: inline-flex; color: ${c.fgMuted}; }
12
16
  & .crisp-toast[data-tone="info"] .crisp-toast-icon { color: ${c.bgBrandSolid}; }
13
17
  & .crisp-toast[data-tone="success"] .crisp-toast-icon { color: ${c.bgSuccess}; }
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  const CrispMessagesContext = React.createContext({});
4
- export function CrispMessagesProvider({ messages, children, }) {
5
- return (_jsx(CrispMessagesContext.Provider, { value: messages, children: children }));
4
+ const CrispComponentDefaultsContext = React.createContext({});
5
+ export function CrispMessagesProvider({ messages, componentDefaults, children, }) {
6
+ return (_jsx(CrispMessagesContext.Provider, { value: messages, children: _jsx(CrispComponentDefaultsContext.Provider, { value: componentDefaults ?? {}, children: children }) }));
6
7
  }
7
8
  /**
8
9
  * Resolves one translatable string for a component: `explicit` is whatever
@@ -35,3 +36,16 @@ export function useCrispMessage(explicit, englishDefault) {
35
36
  export function useCrispMessages() {
36
37
  return React.useContext(CrispMessagesContext);
37
38
  }
39
+ /**
40
+ * Reads one component's own whole-object defaults override, set via
41
+ * `CrispMessagesProvider`'s `componentDefaults` prop (see its own doc for
42
+ * why this exists separately from `useCrispMessage`). `T` is the
43
+ * component's own labels TYPE (e.g. `Partial<RecordPageLabels>`) — this
44
+ * hook itself only knows `name`, a plain string key; the caller supplies
45
+ * and trusts the real shape, the same way `useContext` always has for an
46
+ * untyped-at-the-provider value.
47
+ */
48
+ export function useCrispComponentDefaults(name) {
49
+ const all = React.useContext(CrispComponentDefaultsContext);
50
+ return all[name];
51
+ }
@@ -7,7 +7,7 @@ export { isRtlElement } from "./direction.js";
7
7
  export { FloatingPortal } from "./floating-portal.js";
8
8
  export { hasOpenFloatingLayer, isInsideLaterFloatingLayer, registerFloatingLayer, } from "./floating-stack.js";
9
9
  export { formValueStyle } from "./form-value.js";
10
- export { CrispMessagesProvider, useCrispMessage, useCrispMessages, } from "./i18n.js";
10
+ export { CrispMessagesProvider, useCrispComponentDefaults, useCrispMessage, useCrispMessages, } from "./i18n.js";
11
11
  export { matchMentionTrigger, } from "./mention.js";
12
12
  export { placeFloating, } from "./place-floating.js";
13
13
  export { Primitive } from "./primitive.js";
package/dist/esm/index.js CHANGED
@@ -29,6 +29,7 @@ export * from "./components/fileupload/index.js";
29
29
  export * from "./components/filldoc/index.js";
30
30
  export * from "./components/fillsheet/index.js";
31
31
  export * from "./components/formfield/index.js";
32
+ export * from "./components/heatmapchart/index.js";
32
33
  export * from "./components/input/index.js";
33
34
  export * from "./components/kbd/index.js";
34
35
  export * from "./components/linechart/index.js";
@@ -8,7 +8,7 @@ import { Select } from "../../components/select/index.js";
8
8
  import { Spinner } from "../../components/spinner/index.js";
9
9
  import { Switch } from "../../components/switch/index.js";
10
10
  import { Tabs } from "../../components/tabs/index.js";
11
- import { isRtlElement } from "../../core/index.js";
11
+ import { isRtlElement, useCrispComponentDefaults } from "../../core/index.js";
12
12
  import { editableInputType, tagTone, useEditableField, } from "../recordstable/index.js";
13
13
  import { recordpageRecipe } from "./recordpage.recipe.js";
14
14
  function valueEmpty(v) {
@@ -220,19 +220,34 @@ function EmptyCard({ icon, children, }) {
220
220
  */
221
221
  export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, ...rest }) {
222
222
  const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = useEditableField(onEdit);
223
+ // crisp/#471: a CrispMessagesProvider's `componentDefaults.recordPage` sits
224
+ // BETWEEN the built-in English defaults and this instance's own explicit
225
+ // `labels` prop — a provider translates every RecordPage on the page at
226
+ // once, and a caller who still passes `labels` for one instance keeps
227
+ // overriding it there, same as before this existed. Not `useCrispMessage`:
228
+ // several of these entries (`domainSetTo`, `sortedBy`, …) are formatter
229
+ // FUNCTIONS, which a flat string dictionary can't hold.
230
+ const providerLabels = useCrispComponentDefaults("recordPage");
223
231
  const t = React.useMemo(() => ({
224
232
  ...recordPageLabels,
233
+ ...providerLabels,
225
234
  ...labels,
226
- tabs: { ...recordPageLabels.tabs, ...labels?.tabs },
235
+ tabs: {
236
+ ...recordPageLabels.tabs,
237
+ ...providerLabels?.tabs,
238
+ ...labels?.tabs,
239
+ },
227
240
  highlightLabels: {
228
241
  ...recordPageLabels.highlightLabels,
242
+ ...providerLabels?.highlightLabels,
229
243
  ...labels?.highlightLabels,
230
244
  },
231
245
  highlightEmpty: {
232
246
  ...recordPageLabels.highlightEmpty,
247
+ ...providerLabels?.highlightEmpty,
233
248
  ...labels?.highlightEmpty,
234
249
  },
235
- }), [labels]);
250
+ }), [labels, providerLabels]);
236
251
  const [favorite, setFavorite] = React.useState(false);
237
252
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
238
253
  // `[]`) -- otherwise RecordPage keeps its own self-contained state, as
@@ -14,7 +14,7 @@ import { Menu, MenuItem, useMenuKeyboard } from "../../components/menu/index.js"
14
14
  import { MultiSelect } from "../../components/multiselect/index.js";
15
15
  import { Select } from "../../components/select/index.js";
16
16
  import { Switch } from "../../components/switch/index.js";
17
- import { isInsideLaterFloatingLayer, isRtlElement, usePrefersReducedMotion, } from "../../core/index.js";
17
+ import { isInsideLaterFloatingLayer, isRtlElement, useCrispComponentDefaults, usePrefersReducedMotion, } from "../../core/index.js";
18
18
  import { companyLogos } from "./company-logos.js";
19
19
  import { recordstableRecipe } from "./recordstable.recipe.js";
20
20
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
@@ -456,12 +456,23 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
456
456
  const [filterConds, setFilterConds] = React.useState([]);
457
457
  const [filterQuery, setFilterQuery] = React.useState("");
458
458
  // one popover open at a time: "sort" | "filter" | "more" | `sum:<key>`
459
+ // crisp/#471: same "provider sits between the built-in default and this
460
+ // instance's own explicit labels prop" order as RecordPage's own — see
461
+ // its comment. Several entries here (`selectionAnnouncement`, `sortedBy`,
462
+ // `recordCount`, …) are formatter functions, which is why this goes
463
+ // through `useCrispComponentDefaults`, not `useCrispMessage`.
464
+ const providerLabels = useCrispComponentDefaults("recordsTable");
459
465
  const t = React.useMemo(() => ({
460
466
  ...recordsTableLabels,
467
+ ...providerLabels,
461
468
  ...labels,
462
- agg: { ...recordsTableLabels.agg, ...labels?.agg },
463
- op: { ...recordsTableLabels.op, ...labels?.op },
464
- }), [labels]);
469
+ agg: {
470
+ ...recordsTableLabels.agg,
471
+ ...providerLabels?.agg,
472
+ ...labels?.agg,
473
+ },
474
+ op: { ...recordsTableLabels.op, ...providerLabels?.op, ...labels?.op },
475
+ }), [labels, providerLabels]);
465
476
  const [openMenu, setOpenMenu] = React.useState(null);
466
477
  // the element that opened the current menu, so Esc/select can hand focus back
467
478
  const menuReturn = React.useRef(null);
@@ -134,6 +134,19 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
134
134
  // entirely", the same distinction `updateBar` already makes for the block
135
135
  // menu's search input below.
136
136
  const mentionPickerRef = React.useRef(null);
137
+ // Whether focus currently sits somewhere inside whatever
138
+ // `renderMentionPicker` returned (#514) — tracked through React's own
139
+ // onFocus/onBlur, not `mentionPickerRef.current.contains(activeElement)`.
140
+ // A picker built from crisp's own `Command` (the shape #372's own doc
141
+ // recommends) renders through a `Portal`, so its DOM parent is nowhere
142
+ // near `mentionPickerRef` even though it's still that ref's REACT child;
143
+ // `.contains()` sees a stranger the instant the picker's search input
144
+ // takes focus and updateMention closes the picker it was just asked to
145
+ // protect. React's synthetic focus/blur events bubble through the
146
+ // component tree a portal renders into, not the DOM tree it renders to
147
+ // (documented React behaviour), so a plain onFocus/onBlur pair on this
148
+ // same wrapper sees the portal's contents correctly.
149
+ const mentionPickerHasFocusRef = React.useRef(false);
137
150
  const syncEmpty = (el) => {
138
151
  if (el)
139
152
  el.toggleAttribute("data-empty", (el.textContent ?? "") === "");
@@ -255,6 +268,11 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
255
268
  }, [readOnly, blockMenu, closeBlockMenu]);
256
269
  const closeMention = React.useCallback(() => {
257
270
  mentionRangeRef.current = null;
271
+ // Defensive, not load-bearing: the wrapper's onBlur already clears this
272
+ // when the picker's own focused element leaves the DOM on unmount, but
273
+ // resetting it directly here means a stale `true` can never survive
274
+ // past whatever actually closed the picker, blur-ordering included.
275
+ mentionPickerHasFocusRef.current = false;
258
276
  setMention(null);
259
277
  }, []);
260
278
  /**
@@ -266,16 +284,23 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
266
284
  * no trigger.
267
285
  *
268
286
  * Does NOT auto-close while focus sits inside the consumer's own picker
269
- * (`mentionPickerRef`) — a picker with its own search field would
287
+ * (`mentionPickerHasFocusRef`) — a picker with its own search field would
270
288
  * otherwise close itself the instant that field took focus, since focus
271
289
  * moving there also moves the document selection out of `body`. This is
272
290
  * the same distinction `updateBar` already draws for the block menu's own
273
- * search input, made while merging the two features together.
291
+ * search input, made while merging the two features together — except a
292
+ * consumer's picker, unlike the block menu's own inline search, is free
293
+ * to render through a Portal (crisp's own `Command`, the shape #372's own
294
+ * doc recommends building one from, does exactly that), so this reads a
295
+ * ref React's focus/blur bubbling keeps current instead of asking
296
+ * `mentionPickerRef`'s DOM node whether it contains `activeElement` (#514
297
+ * — a Portal's content is a DOM stranger to its React parent, so that
298
+ * check silently stopped protecting the one case it exists for).
274
299
  */
275
300
  const updateMention = React.useCallback(() => {
276
301
  if (readOnly)
277
302
  return;
278
- if (mentionPickerRef.current?.contains(document.activeElement))
303
+ if (mentionPickerHasFocusRef.current)
279
304
  return;
280
305
  const body = bodyRef.current;
281
306
  const sel = window.getSelection();
@@ -528,7 +553,11 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
528
553
  select: insertMention,
529
554
  close: closeMention,
530
555
  });
531
- return rendered == null ? null : (_jsx("div", { ref: mentionPickerRef, style: { display: "contents" }, children: rendered }));
556
+ return rendered == null ? null : (_jsx("div", { ref: mentionPickerRef, style: { display: "contents" }, onFocus: () => {
557
+ mentionPickerHasFocusRef.current = true;
558
+ }, onBlur: () => {
559
+ mentionPickerHasFocusRef.current = false;
560
+ }, children: rendered }));
532
561
  })()] }));
533
562
  });
534
563
  RichTextEditor.displayName = "RichTextEditor";
@@ -0,0 +1,27 @@
1
+ .crisp-heatmapchart {
2
+ display: flex; gap: var(--crisp-space-2); box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
+ & .crisp-heatmapchart-rowaxis { display: flex; flex-direction: column; justify-content: space-between; padding: var(--crisp-space-1) 0; font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
4
+ & .crisp-heatmapchart-rowaxis-title { writing-mode: vertical-rl; transform: rotate(180deg); align-self: center; }
5
+ & .crisp-heatmapchart-body { flex: 1; }
6
+ & .crisp-heatmapchart-grid { display: grid; gap: var(--crisp-space-1-5); }
7
+ & .crisp-heatmapchart-cell { box-sizing: border-box; display: flex; align-items: center; justify-content: center; aspect-ratio: 2 / 1; border-radius: var(--crisp-radius-md); border: none; background: transparent; margin: 0; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-semibold); font-variant-numeric: tabular-nums; color: var(--crisp-fg-primary); }
8
+ & button.crisp-heatmapchart-cell { cursor: pointer; transition: filter 0.12s ease; }
9
+ & button.crisp-heatmapchart-cell:hover { filter: brightness(0.97); }
10
+ & button.crisp-heatmapchart-cell:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
11
+ & .crisp-heatmapchart-cell[data-tone="neutral"] { background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
12
+ & .crisp-heatmapchart-cell[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); }
13
+ & .crisp-heatmapchart-cell[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); }
14
+ & .crisp-heatmapchart-cell[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 18%, transparent); }
15
+ & .crisp-heatmapchart-cell[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 16%, transparent); }
16
+ & .crisp-heatmapchart-cell[data-ring] { box-shadow: inset 0 0 0 2px var(--crisp-bg-danger-solid); }
17
+ & .crisp-heatmapchart-colaxis { display: flex; justify-content: space-between; margin-top: var(--crisp-space-1-5); padding: 0 var(--crisp-space-0-5); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
18
+ & .crisp-heatmapchart-empty { padding: var(--crisp-space-8) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
19
+ }
20
+
21
+ @media (prefers-reduced-motion: reduce) {
22
+ .crisp-heatmapchart, .crisp-heatmapchart * {
23
+ animation-duration: 0.01ms !important;
24
+ animation-iteration-count: 1 !important;
25
+ transition-duration: 0.01ms !important;
26
+ }
27
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface HeatmapchartVariants {
4
+
5
+ }
6
+
7
+ export declare const heatmapchartRecipe: RecipeFn<{ }>;
@@ -0,0 +1,29 @@
1
+ @layer crisp-components {
2
+ .crisp-heatmapchart {
3
+ display: flex; gap: var(--crisp-space-2); box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ & .crisp-heatmapchart-rowaxis { display: flex; flex-direction: column; justify-content: space-between; padding: var(--crisp-space-1) 0; font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
5
+ & .crisp-heatmapchart-rowaxis-title { writing-mode: vertical-rl; transform: rotate(180deg); align-self: center; }
6
+ & .crisp-heatmapchart-body { flex: 1; }
7
+ & .crisp-heatmapchart-grid { display: grid; gap: var(--crisp-space-1-5); }
8
+ & .crisp-heatmapchart-cell { box-sizing: border-box; display: flex; align-items: center; justify-content: center; aspect-ratio: 2 / 1; border-radius: var(--crisp-radius-md); border: none; background: transparent; margin: 0; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-semibold); font-variant-numeric: tabular-nums; color: var(--crisp-fg-primary); }
9
+ & button.crisp-heatmapchart-cell { cursor: pointer; transition: filter 0.12s ease; }
10
+ & button.crisp-heatmapchart-cell:hover { filter: brightness(0.97); }
11
+ & button.crisp-heatmapchart-cell:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
12
+ & .crisp-heatmapchart-cell[data-tone="neutral"] { background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
13
+ & .crisp-heatmapchart-cell[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); }
14
+ & .crisp-heatmapchart-cell[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); }
15
+ & .crisp-heatmapchart-cell[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 18%, transparent); }
16
+ & .crisp-heatmapchart-cell[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 16%, transparent); }
17
+ & .crisp-heatmapchart-cell[data-ring] { box-shadow: inset 0 0 0 2px var(--crisp-bg-danger-solid); }
18
+ & .crisp-heatmapchart-colaxis { display: flex; justify-content: space-between; margin-top: var(--crisp-space-1-5); padding: 0 var(--crisp-space-0-5); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
19
+ & .crisp-heatmapchart-empty { padding: var(--crisp-space-8) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
20
+ }
21
+
22
+ @media (prefers-reduced-motion: reduce) {
23
+ .crisp-heatmapchart, .crisp-heatmapchart * {
24
+ animation-duration: 0.01ms !important;
25
+ animation-iteration-count: 1 !important;
26
+ transition-duration: 0.01ms !important;
27
+ }
28
+ }
29
+ }
package/dist/styles.css CHANGED
@@ -1028,6 +1028,34 @@
1028
1028
  & .crisp-formfield-error { font-size: var(--crisp-text-sm); line-height: 16px; color: var(--crisp-bg-danger); }
1029
1029
  }
1030
1030
 
1031
+ .crisp-heatmapchart {
1032
+ display: flex; gap: var(--crisp-space-2); box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1033
+ & .crisp-heatmapchart-rowaxis { display: flex; flex-direction: column; justify-content: space-between; padding: var(--crisp-space-1) 0; font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
1034
+ & .crisp-heatmapchart-rowaxis-title { writing-mode: vertical-rl; transform: rotate(180deg); align-self: center; }
1035
+ & .crisp-heatmapchart-body { flex: 1; }
1036
+ & .crisp-heatmapchart-grid { display: grid; gap: var(--crisp-space-1-5); }
1037
+ & .crisp-heatmapchart-cell { box-sizing: border-box; display: flex; align-items: center; justify-content: center; aspect-ratio: 2 / 1; border-radius: var(--crisp-radius-md); border: none; background: transparent; margin: 0; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-semibold); font-variant-numeric: tabular-nums; color: var(--crisp-fg-primary); }
1038
+ & button.crisp-heatmapchart-cell { cursor: pointer; transition: filter 0.12s ease; }
1039
+ & button.crisp-heatmapchart-cell:hover { filter: brightness(0.97); }
1040
+ & button.crisp-heatmapchart-cell:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
1041
+ & .crisp-heatmapchart-cell[data-tone="neutral"] { background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
1042
+ & .crisp-heatmapchart-cell[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); }
1043
+ & .crisp-heatmapchart-cell[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); }
1044
+ & .crisp-heatmapchart-cell[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 18%, transparent); }
1045
+ & .crisp-heatmapchart-cell[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 16%, transparent); }
1046
+ & .crisp-heatmapchart-cell[data-ring] { box-shadow: inset 0 0 0 2px var(--crisp-bg-danger-solid); }
1047
+ & .crisp-heatmapchart-colaxis { display: flex; justify-content: space-between; margin-top: var(--crisp-space-1-5); padding: 0 var(--crisp-space-0-5); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
1048
+ & .crisp-heatmapchart-empty { padding: var(--crisp-space-8) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
1049
+ }
1050
+
1051
+ @media (prefers-reduced-motion: reduce) {
1052
+ .crisp-heatmapchart, .crisp-heatmapchart * {
1053
+ animation-duration: 0.01ms !important;
1054
+ animation-iteration-count: 1 !important;
1055
+ transition-duration: 0.01ms !important;
1056
+ }
1057
+ }
1058
+
1031
1059
  .crisp-input {
1032
1060
  position: relative; box-sizing: border-box; display: inline-flex; align-items: center; width: 100%; background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-field); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px;
1033
1061
  &::after { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 2px solid transparent; transition: border-color .1s ease; pointer-events: none; }
@@ -1758,8 +1786,8 @@
1758
1786
 
1759
1787
  .crisp-toast {
1760
1788
  display: contents; font-family: var(--crisp-font-sans);
1761
- & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); bottom: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
1762
- & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-raised); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 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); } }
1789
+ & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
1790
+ & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
1763
1791
  & .crisp-toast-icon { flex-shrink: 0; margin-top: 1px; display: inline-flex; color: var(--crisp-fg-muted); }
1764
1792
  & .crisp-toast[data-tone="info"] .crisp-toast-icon { color: var(--crisp-bg-brand-solid); }
1765
1793
  & .crisp-toast[data-tone="success"] .crisp-toast-icon { color: var(--crisp-bg-success); }
@@ -1029,6 +1029,34 @@
1029
1029
  & .crisp-formfield-error { font-size: var(--crisp-text-sm); line-height: 16px; color: var(--crisp-bg-danger); }
1030
1030
  }
1031
1031
 
1032
+ .crisp-heatmapchart {
1033
+ display: flex; gap: var(--crisp-space-2); box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1034
+ & .crisp-heatmapchart-rowaxis { display: flex; flex-direction: column; justify-content: space-between; padding: var(--crisp-space-1) 0; font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
1035
+ & .crisp-heatmapchart-rowaxis-title { writing-mode: vertical-rl; transform: rotate(180deg); align-self: center; }
1036
+ & .crisp-heatmapchart-body { flex: 1; }
1037
+ & .crisp-heatmapchart-grid { display: grid; gap: var(--crisp-space-1-5); }
1038
+ & .crisp-heatmapchart-cell { box-sizing: border-box; display: flex; align-items: center; justify-content: center; aspect-ratio: 2 / 1; border-radius: var(--crisp-radius-md); border: none; background: transparent; margin: 0; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-semibold); font-variant-numeric: tabular-nums; color: var(--crisp-fg-primary); }
1039
+ & button.crisp-heatmapchart-cell { cursor: pointer; transition: filter 0.12s ease; }
1040
+ & button.crisp-heatmapchart-cell:hover { filter: brightness(0.97); }
1041
+ & button.crisp-heatmapchart-cell:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
1042
+ & .crisp-heatmapchart-cell[data-tone="neutral"] { background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
1043
+ & .crisp-heatmapchart-cell[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); }
1044
+ & .crisp-heatmapchart-cell[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); }
1045
+ & .crisp-heatmapchart-cell[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 18%, transparent); }
1046
+ & .crisp-heatmapchart-cell[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 16%, transparent); }
1047
+ & .crisp-heatmapchart-cell[data-ring] { box-shadow: inset 0 0 0 2px var(--crisp-bg-danger-solid); }
1048
+ & .crisp-heatmapchart-colaxis { display: flex; justify-content: space-between; margin-top: var(--crisp-space-1-5); padding: 0 var(--crisp-space-0-5); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); text-transform: uppercase; letter-spacing: 0.02em; color: var(--crisp-fg-neutral-subtle); }
1049
+ & .crisp-heatmapchart-empty { padding: var(--crisp-space-8) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
1050
+ }
1051
+
1052
+ @media (prefers-reduced-motion: reduce) {
1053
+ .crisp-heatmapchart, .crisp-heatmapchart * {
1054
+ animation-duration: 0.01ms !important;
1055
+ animation-iteration-count: 1 !important;
1056
+ transition-duration: 0.01ms !important;
1057
+ }
1058
+ }
1059
+
1032
1060
  .crisp-input {
1033
1061
  position: relative; box-sizing: border-box; display: inline-flex; align-items: center; width: 100%; background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-field); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px;
1034
1062
  &::after { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 2px solid transparent; transition: border-color .1s ease; pointer-events: none; }
@@ -1759,8 +1787,8 @@
1759
1787
 
1760
1788
  .crisp-toast {
1761
1789
  display: contents; font-family: var(--crisp-font-sans);
1762
- & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); bottom: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
1763
- & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-raised); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 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); } }
1790
+ & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
1791
+ & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
1764
1792
  & .crisp-toast-icon { flex-shrink: 0; margin-top: 1px; display: inline-flex; color: var(--crisp-fg-muted); }
1765
1793
  & .crisp-toast[data-tone="info"] .crisp-toast-icon { color: var(--crisp-bg-brand-solid); }
1766
1794
  & .crisp-toast[data-tone="success"] .crisp-toast-icon { color: var(--crisp-bg-success); }
package/dist/toast.css CHANGED
@@ -1,7 +1,7 @@
1
1
  .crisp-toast {
2
2
  display: contents; font-family: var(--crisp-font-sans);
3
- & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); bottom: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
4
- & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-raised); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 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); } }
3
+ & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
4
+ & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
5
5
  & .crisp-toast-icon { flex-shrink: 0; margin-top: 1px; display: inline-flex; color: var(--crisp-fg-muted); }
6
6
  & .crisp-toast[data-tone="info"] .crisp-toast-icon { color: var(--crisp-bg-brand-solid); }
7
7
  & .crisp-toast[data-tone="success"] .crisp-toast-icon { color: var(--crisp-bg-success); }
@@ -1,8 +1,8 @@
1
1
  @layer crisp-components {
2
2
  .crisp-toast {
3
3
  display: contents; font-family: var(--crisp-font-sans);
4
- & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); bottom: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
5
- & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-raised); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 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); } }
4
+ & .crisp-toast-viewport { position: fixed; z-index: 80; inset-inline-end: var(--crisp-space-4); top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-2); width: 360px; max-width: calc(100vw - var(--crisp-space-8)); pointer-events: none; }
5
+ & .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); box-sizing: border-box; padding: var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
6
6
  & .crisp-toast-icon { flex-shrink: 0; margin-top: 1px; display: inline-flex; color: var(--crisp-fg-muted); }
7
7
  & .crisp-toast[data-tone="info"] .crisp-toast-icon { color: var(--crisp-bg-brand-solid); }
8
8
  & .crisp-toast[data-tone="success"] .crisp-toast-icon { color: var(--crisp-bg-success); }
@@ -0,0 +1,51 @@
1
+ import type * as React from "react";
2
+ export type HeatmapCell = {
3
+ /** The raw value a consumer's own severity/intensity scale maps from. */
4
+ value: number;
5
+ /** Cell background. "neutral" (the default) is a plain muted tint —
6
+ * typically used for a value of 0. */
7
+ tone?: "neutral" | "success" | "warning" | "orange" | "danger";
8
+ /** Draws an inset danger ring around the cell (e.g. "over the appetite
9
+ * threshold"), independent of — and layered on top of — `tone`. */
10
+ ring?: boolean;
11
+ };
12
+ /** An axis's own end labels + an optional rotated (row) or inline (column)
13
+ * title between them — a 5x5 heatmap reads its axis by its two extremes
14
+ * and what they mean, not a tick per row/column. */
15
+ export type HeatmapAxis = {
16
+ low: React.ReactNode;
17
+ high: React.ReactNode;
18
+ title?: React.ReactNode;
19
+ };
20
+ export type HeatmapChartProps = Omit<React.ComponentPropsWithoutRef<"div">, "children" | "onClick"> & {
21
+ /** `data[row][col]`, row 0 at the top (the axis's `high` end). */
22
+ data: HeatmapCell[][];
23
+ rowAxis: HeatmapAxis;
24
+ colAxis: HeatmapAxis;
25
+ /** Cell display text — default `cell.value || ""` (a 0 renders blank, the
26
+ * same "an empty cell says nothing happened here" reading GRC's own
27
+ * heatmap already used). */
28
+ format?: (cell: HeatmapCell, row: number, col: number) => React.ReactNode;
29
+ /** Hover/focus tooltip text for a cell. */
30
+ cellTitle?: (cell: HeatmapCell, row: number, col: number) => string;
31
+ /** Only cells this returns true for become a real `<button>` — a cell
32
+ * with nothing behind it stays a plain, non-interactive `<div>`. */
33
+ onCellClick?: (row: number, col: number, cell: HeatmapCell) => void;
34
+ isCellClickable?: (cell: HeatmapCell, row: number, col: number) => boolean;
35
+ /** Renders the empty-state message instead of the grid. */
36
+ empty?: boolean;
37
+ emptyLabel?: React.ReactNode;
38
+ "aria-label"?: string;
39
+ className?: string;
40
+ };
41
+ /**
42
+ * An NxM cell-matrix chart — GRC's own risk heatmap (impact x likelihood)
43
+ * is the first consumer. Severity/intensity mapping (`tone`, `ring`) is
44
+ * entirely the caller's own data, not a scale this component invents; see
45
+ * the recipe's own doc comment for why (and for its measurement — or lack
46
+ * of one — against Attio).
47
+ */
48
+ export declare function HeatmapChart({ data, rowAxis, colAxis, format, cellTitle, onCellClick, isCellClickable, empty, emptyLabel, "aria-label": ariaLabel, className, ...rest }: HeatmapChartProps): React.JSX.Element;
49
+ export declare namespace HeatmapChart {
50
+ var displayName: string;
51
+ }
@@ -0,0 +1,3 @@
1
+ declare const config: import("../..").RecipeConfig<{}>;
2
+ export declare const heatmapchartRecipe: import("../..").RecipeFn<{}>;
3
+ export default config;
@@ -0,0 +1 @@
1
+ export * from "./heatmapchart";
@@ -28,8 +28,27 @@ import * as React from "react";
28
28
  * before this existed.
29
29
  */
30
30
  export type CrispMessages = Record<string, string>;
31
- export declare function CrispMessagesProvider({ messages, children, }: {
31
+ /**
32
+ * A handful of product components (`RecordPage`, `RecordsTable`) already
33
+ * take their own translatable text as one big `labels` object rather than a
34
+ * prop per string — and several of ITS entries are formatter FUNCTIONS
35
+ * (`domainSetTo(value)`, `sortedBy(first, extra)`), not plain strings, since
36
+ * word order and pluralisation differ too much across languages for a
37
+ * template (the same reason `TableToolbar.sortTriggerLabel` is a function).
38
+ * `CrispMessages` can't hold those — it's `Record<string,string>` — so those
39
+ * two components get their own whole-object slot instead of a per-string
40
+ * swap through `messages`. `componentDefaults` is deliberately untyped here
41
+ * (`Record<string, unknown>`, cast back to the real shape by
42
+ * `useCrispComponentDefaults<T>` at the CALL site): `core/` sits underneath
43
+ * `product/`, so it cannot import `RecordPageLabels`/`RecordsTableLabels`
44
+ * without a circular dependency. Keyed by the component's own name
45
+ * ("recordPage", "recordsTable") — a small, closed set, not a general
46
+ * registry, since only these two currently need it.
47
+ */
48
+ export type CrispComponentDefaults = Record<string, unknown>;
49
+ export declare function CrispMessagesProvider({ messages, componentDefaults, children, }: {
32
50
  messages: CrispMessages;
51
+ componentDefaults?: CrispComponentDefaults;
33
52
  children?: React.ReactNode;
34
53
  }): React.ReactElement;
35
54
  /**
@@ -56,3 +75,13 @@ export declare function useCrispMessage(explicit: string | undefined, englishDef
56
75
  * per fragment, same fallback order, no hook-in-a-loop.
57
76
  */
58
77
  export declare function useCrispMessages(): CrispMessages;
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
+ export declare function useCrispComponentDefaults<T>(name: string): T | undefined;
@@ -7,7 +7,7 @@ export { isRtlElement } from "./direction";
7
7
  export { FloatingPortal } from "./floating-portal";
8
8
  export { hasOpenFloatingLayer, isInsideLaterFloatingLayer, registerFloatingLayer, } from "./floating-stack";
9
9
  export { formValueStyle } from "./form-value";
10
- export { type CrispMessages, CrispMessagesProvider, useCrispMessage, useCrispMessages, } from "./i18n";
10
+ export { type CrispComponentDefaults, type CrispMessages, CrispMessagesProvider, useCrispComponentDefaults, useCrispMessage, useCrispMessages, } from "./i18n";
11
11
  export { type MentionAnchor, type MentionMatch, matchMentionTrigger, } from "./mention";
12
12
  export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
13
13
  export { Primitive } from "./primitive";
@@ -25,6 +25,7 @@ export * from "./components/fileupload";
25
25
  export * from "./components/filldoc";
26
26
  export * from "./components/fillsheet";
27
27
  export * from "./components/formfield";
28
+ export * from "./components/heatmapchart";
28
29
  export * from "./components/input";
29
30
  export * from "./components/kbd";
30
31
  export * from "./components/linechart";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.53.3",
3
+ "version": "0.53.4",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -168,6 +168,11 @@
168
168
  "import": "./dist/esm/components/formfield/index.js",
169
169
  "require": "./dist/cjs/components/formfield/index.js"
170
170
  },
171
+ "./heatmapchart": {
172
+ "types": "./dist/types/components/heatmapchart/index.d.ts",
173
+ "import": "./dist/esm/components/heatmapchart/index.js",
174
+ "require": "./dist/cjs/components/heatmapchart/index.js"
175
+ },
171
176
  "./input": {
172
177
  "types": "./dist/types/components/input/index.d.ts",
173
178
  "import": "./dist/esm/components/input/index.js",