@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
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { cx } from "../../core/index.js";
4
+ import { cx, useCrispMessage } from "../../core/index.js";
5
5
  import { FillSheet } from "../fillsheet/index.js";
6
6
  import { filldocRecipe } from "./filldoc.recipe.js";
7
7
  /** Size a textarea to its content; see FillSheet's autoGrow for the reset. */
@@ -36,7 +36,12 @@ const emptyTable = () => ({
36
36
  * page covered in handles and grips stops looking like a page, which is the
37
37
  * whole thing this is for.
38
38
  */
39
- export const FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel = "Text", addHeadingLabel = "Heading", addTableLabel = "Table", removeLabel = "Remove", className, style, ...rest }, forwardedRef) {
39
+ export const FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel: addTextLabelProp, addHeadingLabel: addHeadingLabelProp, addTableLabel: addTableLabelProp, removeLabel: removeLabelProp, className, style, ...rest }, forwardedRef) {
40
+ // crisp/#471: same resolution order as Breadcrumb's own.
41
+ const addTextLabel = useCrispMessage(addTextLabelProp, "Text");
42
+ const addHeadingLabel = useCrispMessage(addHeadingLabelProp, "Heading");
43
+ const addTableLabel = useCrispMessage(addTableLabelProp, "Table");
44
+ const removeLabel = useCrispMessage(removeLabelProp, "Remove");
40
45
  const editable = mode === "edit";
41
46
  const replace = (i, block) => {
42
47
  const next = blocks.slice();
@@ -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,7 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCrispMessage } from "../../core/index.js";
2
3
  import { spinnerRecipe } from "./spinner.recipe.js";
3
4
  /** A spinning ring indicator. Reports `role="status"` for assistive tech. */
4
- export function Spinner({ size = 16, "aria-label": ariaLabel = "Loading", className, ...rest }) {
5
+ export function Spinner({ size = 16, "aria-label": ariaLabelProp, className, ...rest }) {
6
+ // crisp/#471: same resolution order as Breadcrumb's own.
7
+ const ariaLabel = useCrispMessage(ariaLabelProp, "Loading");
5
8
  return (_jsx("span", { role: "status", "aria-label": ariaLabel, className: spinnerRecipe({ className }), ...rest, style: {
6
9
  width: size,
7
10
  height: size,
@@ -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";
@@ -78,6 +79,7 @@ export * from "./product/richtext/index.js";
78
79
  export * from "./product/settingssection/index.js";
79
80
  export * from "./product/settingsshell/index.js";
80
81
  export * from "./product/sidebar/index.js";
82
+ export * from "./product/statlist/index.js";
81
83
  export * from "./product/tasklist/index.js";
82
84
  export * from "./product/trialbanner/index.js";
83
85
  export * from "./product/viewtoggle/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 @@
1
+ export * from "./statlist.js";
@@ -0,0 +1,17 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Badge } from "../../components/badge/badge.js";
3
+ import { statlistRecipe } from "./statlist.recipe.js";
4
+ /**
5
+ * A list of label + trailing-count rows — a dashboard's own findings
6
+ * summary ("Open risks with no owner: 3") or an audit-readiness check list,
7
+ * each row optionally a link. Not measured against one specific Attio
8
+ * widget (see the recipe's own doc comment for why); composed from
9
+ * already-measured crisp values throughout.
10
+ */
11
+ export function StatList({ items, emptyLabel = "Nothing to show", className, ...rest }) {
12
+ return (_jsx("div", { className: statlistRecipe({ className }), ...rest, children: items.length === 0 ? (_jsx("div", { className: "crisp-statlist-empty", children: emptyLabel })) : (items.map((item) => {
13
+ const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-statlist-label", children: item.label }), _jsx(Badge, { tone: item.count === 0 ? "neutral" : (item.tone ?? "neutral"), children: item.count })] }));
14
+ return item.href ? (_jsx("a", { href: item.href, onClick: item.onClick, className: "crisp-statlist-row", children: content }, item.id)) : (_jsx("div", { className: "crisp-statlist-row", children: content }, item.id));
15
+ })) }));
16
+ }
17
+ StatList.displayName = "StatList";
@@ -0,0 +1,29 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A list of label + trailing-count rows, each optionally a link — the
4
+ // "N findings of this kind" shape a dashboard's own status summary and an
5
+ // audit-readiness check list both need. No direct Attio widget measures
6
+ // this exact row (Attio's own Reports/Dashboards surface in this workspace
7
+ // has bar/line/number report tiles, not a findings list), so this reuses
8
+ // already-measured crisp values throughout rather than a fresh guess:
9
+ // TaskList's own row-divider structure (border-top, no border on the first
10
+ // row), the type scale's own `md`/`sm` sizes, and Badge's own tone colours
11
+ // for the trailing count — no new pixel or colour value is introduced here.
12
+ const config = defineRecipe({
13
+ name: "statlist",
14
+ base: `display: flex; flex-direction: column; box-sizing: border-box; font-family: ${c.font.sans};
15
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
16
+
17
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["3"]}; padding: ${c.space["2.5"]} 0; border-top: 1px solid ${c.borderSubtle}; font-size: ${c.text.md}; color: inherit; text-decoration: none; }
18
+ & .crisp-statlist-row:first-child { border-top: none; }
19
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: ${c.fgBrandSoft}; }
20
+
21
+ & .crisp-statlist-label { color: ${c.fgMuted}; }
22
+
23
+ & .crisp-statlist-empty { padding: ${c.space["4"]} 0; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }`,
24
+ variants: {},
25
+ defaultVariants: {},
26
+ compoundVariants: [],
27
+ });
28
+ export const statlistRecipe = resolveRecipe(config);
29
+ export default config;
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ .crisp-statlist {
2
+ display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-3); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); color: inherit; text-decoration: none; }
4
+ & .crisp-statlist-row:first-child { border-top: none; }
5
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: var(--crisp-fg-brand-soft); }
6
+ & .crisp-statlist-label { color: var(--crisp-fg-muted); }
7
+ & .crisp-statlist-empty { padding: var(--crisp-space-4) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
8
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface StatlistVariants {
4
+
5
+ }
6
+
7
+ export declare const statlistRecipe: RecipeFn<{ }>;
@@ -0,0 +1,10 @@
1
+ @layer crisp-components {
2
+ .crisp-statlist {
3
+ display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-3); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); color: inherit; text-decoration: none; }
5
+ & .crisp-statlist-row:first-child { border-top: none; }
6
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: var(--crisp-fg-brand-soft); }
7
+ & .crisp-statlist-label { color: var(--crisp-fg-muted); }
8
+ & .crisp-statlist-empty { padding: var(--crisp-space-4) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
9
+ }
10
+ }
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); }
@@ -2729,6 +2757,15 @@
2729
2757
  }
2730
2758
  }
2731
2759
 
2760
+ .crisp-statlist {
2761
+ display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2762
+ & .crisp-statlist-row { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-3); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); color: inherit; text-decoration: none; }
2763
+ & .crisp-statlist-row:first-child { border-top: none; }
2764
+ & a.crisp-statlist-row:hover .crisp-statlist-label { color: var(--crisp-fg-brand-soft); }
2765
+ & .crisp-statlist-label { color: var(--crisp-fg-muted); }
2766
+ & .crisp-statlist-empty { padding: var(--crisp-space-4) 0; text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
2767
+ }
2768
+
2732
2769
  .crisp-tasklist {
2733
2770
  display: flex; flex-direction: column; box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2734
2771
  & .crisp-tasklist-group { display: flex; align-items: center; gap: var(--crisp-space-1-5); height: 36px; padding: 0 var(--crisp-space-2); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }