@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.
- package/dist/cjs/components/heatmapchart/heatmapchart.js +24 -0
- package/dist/cjs/components/heatmapchart/heatmapchart.recipe.js +49 -0
- package/dist/cjs/components/heatmapchart/index.js +17 -0
- package/dist/cjs/components/toast/toast.recipe.js +8 -4
- package/dist/cjs/core/i18n.js +17 -2
- package/dist/cjs/core/index.js +2 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +17 -2
- package/dist/cjs/product/recordstable/recordstable.js +14 -3
- package/dist/cjs/product/richtext/richtext.js +33 -4
- package/dist/esm/components/heatmapchart/heatmapchart.js +21 -0
- package/dist/esm/components/heatmapchart/heatmapchart.recipe.js +46 -0
- package/dist/esm/components/heatmapchart/index.js +1 -0
- package/dist/esm/components/toast/toast.recipe.js +9 -5
- package/dist/esm/core/i18n.js +16 -2
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +18 -3
- package/dist/esm/product/recordstable/recordstable.js +15 -4
- package/dist/esm/product/richtext/richtext.js +33 -4
- package/dist/heatmapchart.css +27 -0
- package/dist/heatmapchart.d.ts +7 -0
- package/dist/heatmapchart.layered.css +29 -0
- package/dist/styles.css +30 -2
- package/dist/styles.layered.css +30 -2
- package/dist/toast.css +2 -2
- package/dist/toast.layered.css +2 -2
- package/dist/types/components/heatmapchart/heatmapchart.d.ts +51 -0
- package/dist/types/components/heatmapchart/heatmapchart.recipe.d.ts +3 -0
- package/dist/types/components/heatmapchart/index.d.ts +1 -0
- package/dist/types/core/i18n.d.ts +30 -1
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- 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
|
|
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"]};
|
|
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["
|
|
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}; }
|
package/dist/cjs/core/i18n.js
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
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
|
+
}
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -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: {
|
|
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: {
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
* (`
|
|
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 (
|
|
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" },
|
|
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
|
|
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"]};
|
|
10
|
-
& .crisp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: ${c.space["2.5"]}; box-sizing: border-box; padding: ${c.space["
|
|
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}; }
|
package/dist/esm/core/i18n.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
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
|
+
}
|
package/dist/esm/core/index.js
CHANGED
|
@@ -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: {
|
|
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: {
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
* (`
|
|
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 (
|
|
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" },
|
|
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,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);
|
|
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-
|
|
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); }
|
package/dist/styles.layered.css
CHANGED
|
@@ -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);
|
|
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-
|
|
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);
|
|
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
|
+
& .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); }
|
package/dist/toast.layered.css
CHANGED
|
@@ -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);
|
|
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-
|
|
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 @@
|
|
|
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
|
-
|
|
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";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
+
"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",
|