@crisp-ui-kit/crisp 0.53.5 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/bulkbar/bulkbar.js +5 -1
- package/dist/cjs/components/fileupload/fileupload.js +19 -3
- package/dist/cjs/components/formfield/formfield.js +8 -1
- package/dist/cjs/components/heatmapchart/heatmapchart.js +14 -2
- package/dist/cjs/components/skiplink/index.js +17 -0
- package/dist/cjs/components/skiplink/skiplink.js +16 -0
- package/dist/cjs/components/skiplink/skiplink.recipe.js +33 -0
- package/dist/cjs/components/textlink/index.js +7 -0
- package/dist/cjs/components/textlink/textlink.js +16 -0
- package/dist/cjs/components/textlink/textlink.recipe.js +35 -0
- package/dist/cjs/components/visuallyhidden/index.js +17 -0
- package/dist/cjs/components/visuallyhidden/visuallyhidden.js +16 -0
- package/dist/cjs/components/visuallyhidden/visuallyhidden.recipe.js +20 -0
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/product/board/board.js +11 -2
- package/dist/cjs/product/command/command.js +13 -1
- package/dist/cjs/product/flowcanvas/flowcanvas.js +9 -2
- package/dist/cjs/product/headerbar/headerbar.js +15 -0
- package/dist/cjs/product/headerbar/headerbar.recipe.js +35 -0
- package/dist/cjs/product/headerbar/index.js +17 -0
- package/dist/cjs/product/inbox/inbox.js +13 -1
- package/dist/cjs/product/onboarding/onboarding.js +22 -1
- package/dist/cjs/product/recordpage/index.js +2 -1
- package/dist/cjs/product/recordpage/recordpage.js +197 -123
- package/dist/cjs/product/recordpage/recordpage.recipe.js +5 -0
- package/dist/cjs/product/tasklist/tasklist.js +8 -1
- package/dist/esm/components/bulkbar/bulkbar.js +5 -1
- package/dist/esm/components/fileupload/fileupload.js +20 -4
- package/dist/esm/components/formfield/formfield.js +8 -1
- package/dist/esm/components/heatmapchart/heatmapchart.js +14 -2
- package/dist/esm/components/skiplink/index.js +1 -0
- package/dist/esm/components/skiplink/skiplink.js +13 -0
- package/dist/esm/components/skiplink/skiplink.recipe.js +30 -0
- package/dist/esm/components/textlink/index.js +2 -0
- package/dist/esm/components/textlink/textlink.js +13 -0
- package/dist/esm/components/textlink/textlink.recipe.js +32 -0
- package/dist/esm/components/visuallyhidden/index.js +1 -0
- package/dist/esm/components/visuallyhidden/visuallyhidden.js +13 -0
- package/dist/esm/components/visuallyhidden/visuallyhidden.recipe.js +17 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/product/board/board.js +12 -3
- package/dist/esm/product/command/command.js +14 -2
- package/dist/esm/product/flowcanvas/flowcanvas.js +9 -2
- package/dist/esm/product/headerbar/headerbar.js +12 -0
- package/dist/esm/product/headerbar/headerbar.recipe.js +32 -0
- package/dist/esm/product/headerbar/index.js +1 -0
- package/dist/esm/product/inbox/inbox.js +13 -1
- package/dist/esm/product/onboarding/onboarding.js +22 -1
- package/dist/esm/product/recordpage/index.js +1 -1
- package/dist/esm/product/recordpage/recordpage.js +197 -124
- package/dist/esm/product/recordpage/recordpage.recipe.js +5 -0
- package/dist/esm/product/tasklist/tasklist.js +8 -1
- package/dist/headerbar.css +7 -0
- package/dist/headerbar.d.ts +7 -0
- package/dist/headerbar.layered.css +9 -0
- package/dist/recordpage.css +17 -31
- package/dist/recordpage.layered.css +17 -31
- package/dist/skiplink.css +5 -0
- package/dist/skiplink.d.ts +7 -0
- package/dist/skiplink.layered.css +7 -0
- package/dist/styles.css +47 -31
- package/dist/styles.layered.css +47 -31
- package/dist/textlink.css +11 -0
- package/dist/textlink.d.ts +7 -0
- package/dist/textlink.layered.css +13 -0
- package/dist/types/components/bulkbar/bulkbar.d.ts +1 -1
- package/dist/types/components/heatmapchart/heatmapchart.d.ts +1 -1
- package/dist/types/components/skiplink/index.d.ts +1 -0
- package/dist/types/components/skiplink/skiplink.d.ts +18 -0
- package/dist/types/components/skiplink/skiplink.recipe.d.ts +3 -0
- package/dist/types/components/textlink/index.d.ts +2 -0
- package/dist/types/components/textlink/textlink.d.ts +19 -0
- package/dist/types/components/textlink/textlink.recipe.d.ts +13 -0
- package/dist/types/components/visuallyhidden/index.d.ts +1 -0
- package/dist/types/components/visuallyhidden/visuallyhidden.d.ts +15 -0
- package/dist/types/components/visuallyhidden/visuallyhidden.recipe.d.ts +3 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/product/board/board.d.ts +1 -1
- package/dist/types/product/command/command.d.ts +1 -1
- package/dist/types/product/flowcanvas/flowcanvas.d.ts +1 -1
- package/dist/types/product/headerbar/headerbar.d.ts +27 -0
- package/dist/types/product/headerbar/headerbar.recipe.d.ts +3 -0
- package/dist/types/product/headerbar/index.d.ts +1 -0
- package/dist/types/product/inbox/inbox.d.ts +1 -1
- package/dist/types/product/onboarding/onboarding.d.ts +1 -1
- package/dist/types/product/recordpage/index.d.ts +1 -1
- package/dist/types/product/recordpage/recordpage.d.ts +90 -1
- package/dist/types/product/tasklist/tasklist.d.ts +1 -1
- package/dist/visuallyhidden.css +3 -0
- package/dist/visuallyhidden.d.ts +7 -0
- package/dist/visuallyhidden.layered.css +5 -0
- package/package.json +21 -1
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.recordPageLabels = void 0;
|
|
37
|
+
exports.RecordDetails = RecordDetails;
|
|
37
38
|
exports.RecordPage = RecordPage;
|
|
38
39
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
39
40
|
const lucide_react_1 = require("lucide-react");
|
|
@@ -124,7 +125,42 @@ exports.recordPageLabels = {
|
|
|
124
125
|
taggedCategories: (count) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Tagged ", (0, jsx_runtime_1.jsx)("b", { children: count }), " categories"] })),
|
|
125
126
|
recordCreated: (name) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("b", { children: name }), " record was created"] })),
|
|
126
127
|
noEmailsWith: (name) => (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["No emails logged with ", name, " yet."] }),
|
|
128
|
+
noFieldsMatch: (query) => (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["No fields match \u201C", query, "\u201D."] }),
|
|
127
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* crisp/#471: a `CrispMessagesProvider`'s `componentDefaults.recordPage` sits
|
|
132
|
+
* BETWEEN the built-in English defaults and an instance's own explicit
|
|
133
|
+
* `labels` prop -- a provider translates every `RecordPage`/`RecordDetails`
|
|
134
|
+
* on the page at once, and a caller who still passes `labels` for one
|
|
135
|
+
* instance keeps overriding it there. Not `useCrispMessage`: several of
|
|
136
|
+
* these entries (`domainSetTo`, `noFieldsMatch`, …) are formatter FUNCTIONS,
|
|
137
|
+
* which a flat string dictionary can't hold. Shared by `RecordPage` and
|
|
138
|
+
* `RecordDetails` (#528) -- both merge the same three layers the same way,
|
|
139
|
+
* so this is the one place that merge is written, not two copies of it.
|
|
140
|
+
*/
|
|
141
|
+
function useRecordPageLabels(labels) {
|
|
142
|
+
const providerLabels = (0, core_1.useCrispComponentDefaults)("recordPage");
|
|
143
|
+
return React.useMemo(() => ({
|
|
144
|
+
...exports.recordPageLabels,
|
|
145
|
+
...providerLabels,
|
|
146
|
+
...labels,
|
|
147
|
+
tabs: {
|
|
148
|
+
...exports.recordPageLabels.tabs,
|
|
149
|
+
...providerLabels?.tabs,
|
|
150
|
+
...labels?.tabs,
|
|
151
|
+
},
|
|
152
|
+
highlightLabels: {
|
|
153
|
+
...exports.recordPageLabels.highlightLabels,
|
|
154
|
+
...providerLabels?.highlightLabels,
|
|
155
|
+
...labels?.highlightLabels,
|
|
156
|
+
},
|
|
157
|
+
highlightEmpty: {
|
|
158
|
+
...exports.recordPageLabels.highlightEmpty,
|
|
159
|
+
...providerLabels?.highlightEmpty,
|
|
160
|
+
...labels?.highlightEmpty,
|
|
161
|
+
},
|
|
162
|
+
}), [labels, providerLabels]);
|
|
163
|
+
}
|
|
128
164
|
/** `Select` owns its own closed-by-default `open` state with no prop or
|
|
129
165
|
* imperative way to start it open -- the only thing that opens it is a real
|
|
130
166
|
* click on its trigger button. Mounting one on click (rather than rendering
|
|
@@ -180,6 +216,16 @@ function TagsValue({ values }) {
|
|
|
180
216
|
}
|
|
181
217
|
function FieldValue({ col, row, labels, }) {
|
|
182
218
|
const v = row[col.key];
|
|
219
|
+
// `render` (#528) -- the same full-custom-content escape hatch
|
|
220
|
+
// `RecordsTable`'s own columns already have (recordstable.tsx's own doc:
|
|
221
|
+
// "an escape hatch for a display crisp can't express via type"), reused
|
|
222
|
+
// here so a value this component has no built-in shape for (a reference
|
|
223
|
+
// field's link to another record, say) doesn't need a new `type`/`editor`
|
|
224
|
+
// of its own. Checked before the empty check -- like `RecordsTable`'s own
|
|
225
|
+
// `render`, it owns the cell's content fully, including what "empty"
|
|
226
|
+
// looks like for that column.
|
|
227
|
+
if (col.render)
|
|
228
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: col.render(v, row) });
|
|
183
229
|
if (valueEmpty(v))
|
|
184
230
|
return (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-empty", children: labels.setAValue });
|
|
185
231
|
// `editor: "select"` at rest (#457) -- checked ahead of `col.type` since
|
|
@@ -247,6 +293,147 @@ function TasksSection({ tasks, onAdd, onToggle, labels, }) {
|
|
|
247
293
|
function EmptyCard({ icon, children, }) {
|
|
248
294
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-emptycard", children: [icon, children] }));
|
|
249
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* The Details column's own field list, on its own (#528) — `RecordPage`'s
|
|
298
|
+
* "Record Details" collapsible + searchable field list, the one piece a
|
|
299
|
+
* consumer with a fixed-width rail of its own (no resize handle, no entity
|
|
300
|
+
* header, no CRM Lists panel — GRC's own persistent 320px column, grc#66)
|
|
301
|
+
* actually needs, rather than rebuilding this same click-to-edit/search/
|
|
302
|
+
* `sections` machinery by hand or pulling in the whole page's chrome for a
|
|
303
|
+
* rail that isn't one. `RecordPage` composes this internally for its own
|
|
304
|
+
* resizable aside (same component either way, not a second copy) — this is
|
|
305
|
+
* that same block, exactly, just not wrapped in `RecordPage`'s own topbar/
|
|
306
|
+
* tabs/resizer.
|
|
307
|
+
*
|
|
308
|
+
* Not wrapped in `recordpageRecipe` itself: doing so would duplicate the
|
|
309
|
+
* recipe's own `display:flex;height:100%` base rule as a nested child
|
|
310
|
+
* inside `RecordPage`'s aside (which already carries that class from
|
|
311
|
+
* `RecordPage`'s own root), breaking that layout. A standalone caller
|
|
312
|
+
* needs exactly one `.crisp-recordpage`-scoped ancestor for this
|
|
313
|
+
* component's classes to resolve — wrap it in one, the same way
|
|
314
|
+
* `RecordPage`'s own root does: `<div className={recordpageRecipe()}>`
|
|
315
|
+
* (also exported from this package).
|
|
316
|
+
*/
|
|
317
|
+
function RecordDetails({ record, columns, sections, onEdit, labels, className, ...rest }) {
|
|
318
|
+
const t = useRecordPageLabels(labels);
|
|
319
|
+
const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
|
|
320
|
+
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
321
|
+
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
322
|
+
const [fieldQuery, setFieldQuery] = React.useState("");
|
|
323
|
+
const [showAllValues, setShowAllValues] = React.useState(false);
|
|
324
|
+
const fields = columns.filter((col) => col.type !== "entity");
|
|
325
|
+
const q = fieldQuery.trim().toLowerCase();
|
|
326
|
+
const matches = (col) => !q || col.label.toLowerCase().includes(q);
|
|
327
|
+
// #528: `sections` groups fields under headings, in the caller's own
|
|
328
|
+
// order, and always shows every match — no 5-field reveal (see
|
|
329
|
+
// `RecordPageSection`'s own doc). Omitted, this stays the flat,
|
|
330
|
+
// Attio-measured list with its 5-field "View all values" reveal,
|
|
331
|
+
// unchanged from before #528.
|
|
332
|
+
const FIELD_LIMIT = 5;
|
|
333
|
+
let canRevealFields = false;
|
|
334
|
+
let groups;
|
|
335
|
+
if (sections) {
|
|
336
|
+
const byKey = new Map(fields.map((col) => [col.key, col]));
|
|
337
|
+
groups = sections
|
|
338
|
+
.map((s) => ({
|
|
339
|
+
title: s.title,
|
|
340
|
+
fields: s.fields
|
|
341
|
+
.map((key) => byKey.get(key))
|
|
342
|
+
.filter((col) => col !== undefined)
|
|
343
|
+
.filter(matches),
|
|
344
|
+
}))
|
|
345
|
+
// A section left with no matching fields (a search that matches
|
|
346
|
+
// nothing in it, or a `fields` key naming no real column) doesn't
|
|
347
|
+
// render at all -- heading included, not an empty group under a
|
|
348
|
+
// heading with nothing below it.
|
|
349
|
+
.filter((g) => g.fields.length > 0);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
const matched = q ? fields.filter(matches) : fields;
|
|
353
|
+
canRevealFields = !q && matched.length > FIELD_LIMIT;
|
|
354
|
+
const shown = canRevealFields && !showAllValues
|
|
355
|
+
? matched.slice(0, FIELD_LIMIT)
|
|
356
|
+
: matched;
|
|
357
|
+
groups = [{ title: undefined, fields: shown }];
|
|
358
|
+
}
|
|
359
|
+
const totalShown = groups.reduce((n, g) => n + g.fields.length, 0);
|
|
360
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, core_1.cx)("crisp-recordpage-section", className), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? ((0, jsx_runtime_1.jsx)("input", {
|
|
361
|
+
// biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
|
|
362
|
+
autoFocus: true, type: "text", className: "crisp-recordpage-fieldsearch", placeholder: t.searchPlaceholder, "aria-label": t.searchFields, value: fieldQuery, onChange: (e) => setFieldQuery(e.target.value), onKeyDown: (e) => {
|
|
363
|
+
if (e.key === "Escape") {
|
|
364
|
+
setSearchOpen(false);
|
|
365
|
+
setFieldQuery("");
|
|
366
|
+
}
|
|
367
|
+
} })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-collapse", "data-collapsed": detailsOpen ? undefined : "", "aria-expanded": detailsOpen, onClick: () => setDetailsOpen((o) => !o), children: [t.recordDetails, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-recordpage-chev", size: 12, strokeWidth: 2.5 })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-detailsearch", "data-active": searchOpen ? "" : undefined, "aria-label": searchOpen ? t.closeSearch : t.searchFields, "aria-pressed": searchOpen, onClick: () => {
|
|
368
|
+
setSearchOpen((s) => !s);
|
|
369
|
+
setFieldQuery("");
|
|
370
|
+
if (!searchOpen)
|
|
371
|
+
setDetailsOpen(true);
|
|
372
|
+
}, children: searchOpen ? ((0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 14, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 14, strokeWidth: 1.75 })) })] }), detailsOpen &&
|
|
373
|
+
(totalShown > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [groups.map((g, i) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [sections && g.title != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-section-title crisp-recordpage-fieldsection-title", children: g.title })), g.fields.map((col) => {
|
|
374
|
+
// #431: a field only actually opens when there's an
|
|
375
|
+
// `onEdit` to commit through -- `editable` with nothing
|
|
376
|
+
// to call would be a dead click.
|
|
377
|
+
const canEdit = Boolean(col.editable && onEdit);
|
|
378
|
+
const isPending = pendingKey === col.key;
|
|
379
|
+
const fieldError = error?.key === col.key ? error.message : null;
|
|
380
|
+
const label = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-label", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
|
|
381
|
+
// `switch` is always its own live widget -- no
|
|
382
|
+
// click-to-open, no separate edit/view mode: flipping the
|
|
383
|
+
// toggle IS the commit gesture, and there's no
|
|
384
|
+
// meaningfully different "rest" display for a boolean
|
|
385
|
+
// (same exception `DataTable`'s own `editor` makes, #453).
|
|
386
|
+
if (canEdit && col.editor === "switch") {
|
|
387
|
+
const value = record[col.key];
|
|
388
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
389
|
+
}
|
|
390
|
+
// `select` opens crisp's own `Select` on a SINGLE click
|
|
391
|
+
// (#457, corrects #431/#385's original always-live copy
|
|
392
|
+
// of the same regression #453 fixed on `DataTable`) --
|
|
393
|
+
// like every other editable type here, at rest it shows
|
|
394
|
+
// its value (see `FieldValue`'s own `col.editor ===
|
|
395
|
+
// "select"` case) via the generic click-to-open branch
|
|
396
|
+
// below, and only mounts `Select` once `editingKey`
|
|
397
|
+
// matches. `DataTable`'s own double-click gate doesn't
|
|
398
|
+
// apply: `RecordPage`'s plain text/date fields already
|
|
399
|
+
// open on a single click (measured live for #431), so
|
|
400
|
+
// `select` matches that, not `DataTable`.
|
|
401
|
+
if (canEdit &&
|
|
402
|
+
editingKey === col.key &&
|
|
403
|
+
col.editor === "select") {
|
|
404
|
+
const value = record[col.key];
|
|
405
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
406
|
+
}
|
|
407
|
+
if (canEdit && editingKey === col.key) {
|
|
408
|
+
const value = record[col.key];
|
|
409
|
+
const editor = (0, recordstable_1.editableInputType)(col.type);
|
|
410
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-recordpage-edit",
|
|
411
|
+
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
412
|
+
autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
|
|
413
|
+
if (isPending)
|
|
414
|
+
return;
|
|
415
|
+
if (e.key === "Enter")
|
|
416
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
417
|
+
else if (e.key === "Escape")
|
|
418
|
+
closeEditing();
|
|
419
|
+
}, onBlur: (e) => {
|
|
420
|
+
if (isPending)
|
|
421
|
+
return;
|
|
422
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
423
|
+
} }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
424
|
+
}
|
|
425
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t }) })) : ((0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
426
|
+
})] }, g.title != null ? String(g.title) : `section-${i}`))), canRevealFields && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
|
|
427
|
+
// Revealing all fields also opens the (focused) field
|
|
428
|
+
// search; collapsing closes it.
|
|
429
|
+
const next = !showAllValues;
|
|
430
|
+
setShowAllValues(next);
|
|
431
|
+
setSearchOpen(next);
|
|
432
|
+
if (!next)
|
|
433
|
+
setFieldQuery("");
|
|
434
|
+
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) })))] }));
|
|
435
|
+
}
|
|
436
|
+
RecordDetails.displayName = "RecordDetails";
|
|
250
437
|
/**
|
|
251
438
|
* The full-page company record view — the page that opens when you click into a
|
|
252
439
|
* record (as opposed to the peek [RecordPanel]). A top breadcrumb bar with
|
|
@@ -255,36 +442,8 @@ function EmptyCard({ icon, children, }) {
|
|
|
255
442
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
256
443
|
* into the app's main content area.
|
|
257
444
|
*/
|
|
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
|
-
const
|
|
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");
|
|
268
|
-
const t = React.useMemo(() => ({
|
|
269
|
-
...exports.recordPageLabels,
|
|
270
|
-
...providerLabels,
|
|
271
|
-
...labels,
|
|
272
|
-
tabs: {
|
|
273
|
-
...exports.recordPageLabels.tabs,
|
|
274
|
-
...providerLabels?.tabs,
|
|
275
|
-
...labels?.tabs,
|
|
276
|
-
},
|
|
277
|
-
highlightLabels: {
|
|
278
|
-
...exports.recordPageLabels.highlightLabels,
|
|
279
|
-
...providerLabels?.highlightLabels,
|
|
280
|
-
...labels?.highlightLabels,
|
|
281
|
-
},
|
|
282
|
-
highlightEmpty: {
|
|
283
|
-
...exports.recordPageLabels.highlightEmpty,
|
|
284
|
-
...providerLabels?.highlightEmpty,
|
|
285
|
-
...labels?.highlightEmpty,
|
|
286
|
-
},
|
|
287
|
-
}), [labels, providerLabels]);
|
|
445
|
+
function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, ...rest }) {
|
|
446
|
+
const t = useRecordPageLabels(labels);
|
|
288
447
|
const [favorite, setFavorite] = React.useState(false);
|
|
289
448
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
290
449
|
// `[]`) -- otherwise RecordPage keeps its own self-contained state, as
|
|
@@ -296,11 +455,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
296
455
|
const notes = notesProp ?? internalNotes;
|
|
297
456
|
const tasks = tasksProp ?? internalTasks;
|
|
298
457
|
const [showAllActivity, setShowAllActivity] = React.useState(false);
|
|
299
|
-
// Collapsible + searchable Record Details.
|
|
300
|
-
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
301
|
-
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
302
|
-
const [fieldQuery, setFieldQuery] = React.useState("");
|
|
303
|
-
const [showAllValues, setShowAllValues] = React.useState(false);
|
|
304
458
|
// Drag-resizable left details column.
|
|
305
459
|
const [detailsW, setDetailsW] = React.useState(320);
|
|
306
460
|
const [resizing, setResizing] = React.useState(false);
|
|
@@ -337,17 +491,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
337
491
|
? record[entityCol.key]
|
|
338
492
|
: undefined;
|
|
339
493
|
const name = entity?.name ?? "Record";
|
|
340
|
-
const fields = columns.filter((col) => col.type !== "entity");
|
|
341
|
-
const q = fieldQuery.trim().toLowerCase();
|
|
342
|
-
const matchedFields = q
|
|
343
|
-
? fields.filter((col) => col.label.toLowerCase().includes(q))
|
|
344
|
-
: fields;
|
|
345
|
-
// Shows the first 5 fields; "View all values" reveals the rest.
|
|
346
|
-
const FIELD_LIMIT = 5;
|
|
347
|
-
const canRevealFields = !q && matchedFields.length > FIELD_LIMIT;
|
|
348
|
-
const shownFields = canRevealFields && !showAllValues
|
|
349
|
-
? matchedFields.slice(0, FIELD_LIMIT)
|
|
350
|
-
: matchedFields;
|
|
351
494
|
const byKey = React.useMemo(() => new Map(columns.map((col) => [col.key, col])), [columns]);
|
|
352
495
|
// The Overview "Highlights" — a fixed set of enrichment stats (measured
|
|
353
496
|
// from the live app), a couple derived from the record, the rest empty.
|
|
@@ -365,7 +508,11 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
365
508
|
: n >= 1000
|
|
366
509
|
? "1K–10K"
|
|
367
510
|
: "<1K";
|
|
368
|
-
|
|
511
|
+
// #528: the built-in CRM Highlights set, used when the caller omits
|
|
512
|
+
// `highlights` entirely -- a caller passing `highlights` (an array, or
|
|
513
|
+
// `false` to omit the section) replaces this wholesale, the same
|
|
514
|
+
// "caller's own list, not a merge" choice `tabs` already makes (#379).
|
|
515
|
+
const defaultHighlights = [
|
|
369
516
|
{
|
|
370
517
|
label: t.highlightLabels.connectionStrength,
|
|
371
518
|
icon: lucide_react_1.Zap,
|
|
@@ -403,6 +550,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
403
550
|
empty: t.highlightEmpty.employeeRange,
|
|
404
551
|
},
|
|
405
552
|
];
|
|
553
|
+
const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
|
|
406
554
|
const ti = { size: 15, strokeWidth: 1.75 };
|
|
407
555
|
const builtinTabs = [
|
|
408
556
|
{ value: "overview", label: t.tabs.overview, icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { ...ti }) },
|
|
@@ -485,7 +633,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
485
633
|
const builtinPanel = (active) => {
|
|
486
634
|
switch (active) {
|
|
487
635
|
case "overview":
|
|
488
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-panel", children: [(0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-section-title", children: t.highlights }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-card", children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-hl-cell", children: [(0, jsx_runtime_1.jsx)(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-label", children: h.label }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-value", children: h.value ?? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] }), activityFeed, (0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), (0, jsx_runtime_1.jsx)(NotesSection, { notes: notes, onAdd: addNote, labels: t }), (0, jsx_runtime_1.jsx)(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
|
|
636
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-panel", children: [highlights !== false && ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-section-title", children: t.highlights }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-card", children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-hl-cell", children: [h.icon && ((0, jsx_runtime_1.jsx)(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 })), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-label", children: h.label }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-value", children: h.value ?? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] })), activityFeed, (0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), (0, jsx_runtime_1.jsx)(NotesSection, { notes: notes, onAdd: addNote, labels: t }), (0, jsx_runtime_1.jsx)(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
|
|
489
637
|
case "activity":
|
|
490
638
|
return (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: activityFeed });
|
|
491
639
|
case "notes":
|
|
@@ -511,81 +659,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
511
659
|
const effectivePanel = tabsProp
|
|
512
660
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
513
661
|
: builtinPanel;
|
|
514
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsxs)("
|
|
515
|
-
// biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
|
|
516
|
-
autoFocus: true, type: "text", className: "crisp-recordpage-fieldsearch", placeholder: t.searchPlaceholder, "aria-label": t.searchFields, value: fieldQuery, onChange: (e) => setFieldQuery(e.target.value), onKeyDown: (e) => {
|
|
517
|
-
if (e.key === "Escape") {
|
|
518
|
-
setSearchOpen(false);
|
|
519
|
-
setFieldQuery("");
|
|
520
|
-
}
|
|
521
|
-
} })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-collapse", "data-collapsed": detailsOpen ? undefined : "", "aria-expanded": detailsOpen, onClick: () => setDetailsOpen((o) => !o), children: [t.recordDetails, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-recordpage-chev", size: 12, strokeWidth: 2.5 })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-detailsearch", "data-active": searchOpen ? "" : undefined, "aria-label": searchOpen ? t.closeSearch : t.searchFields, "aria-pressed": searchOpen, onClick: () => {
|
|
522
|
-
setSearchOpen((s) => !s);
|
|
523
|
-
setFieldQuery("");
|
|
524
|
-
if (!searchOpen)
|
|
525
|
-
setDetailsOpen(true);
|
|
526
|
-
}, children: searchOpen ? ((0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 14, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 14, strokeWidth: 1.75 })) })] }), detailsOpen &&
|
|
527
|
-
(shownFields.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shownFields.map((col) => {
|
|
528
|
-
// #431: a field only actually opens when there's an
|
|
529
|
-
// `onEdit` to commit through -- `editable` with nothing
|
|
530
|
-
// to call would be a dead click.
|
|
531
|
-
const canEdit = Boolean(col.editable && onEdit);
|
|
532
|
-
const isPending = pendingKey === col.key;
|
|
533
|
-
const fieldError = error?.key === col.key ? error.message : null;
|
|
534
|
-
const label = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-label", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
|
|
535
|
-
// `switch` is always its own live widget -- no
|
|
536
|
-
// click-to-open, no separate edit/view mode: flipping the
|
|
537
|
-
// toggle IS the commit gesture, and there's no
|
|
538
|
-
// meaningfully different "rest" display for a boolean
|
|
539
|
-
// (same exception `DataTable`'s own `editor` makes, #453).
|
|
540
|
-
if (canEdit && col.editor === "switch") {
|
|
541
|
-
const value = record[col.key];
|
|
542
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
543
|
-
}
|
|
544
|
-
// `select` opens crisp's own `Select` on a SINGLE click
|
|
545
|
-
// (#457, corrects #431/#385's original always-live
|
|
546
|
-
// copy of the same regression #453 fixed on `DataTable`)
|
|
547
|
-
// -- like every other editable type here, at rest it
|
|
548
|
-
// shows its value (see `FieldValue`'s own `col.editor
|
|
549
|
-
// === "select"` case) via the generic click-to-open
|
|
550
|
-
// branch below, and only mounts `Select` once
|
|
551
|
-
// `editingKey` matches. `DataTable`'s own double-click
|
|
552
|
-
// gate doesn't apply: `RecordPage`'s plain text/date
|
|
553
|
-
// fields already open on a single click (measured live
|
|
554
|
-
// for #431), so `select` matches that, not `DataTable`.
|
|
555
|
-
if (canEdit &&
|
|
556
|
-
editingKey === col.key &&
|
|
557
|
-
col.editor === "select") {
|
|
558
|
-
const value = record[col.key];
|
|
559
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
560
|
-
}
|
|
561
|
-
if (canEdit && editingKey === col.key) {
|
|
562
|
-
const value = record[col.key];
|
|
563
|
-
const editor = (0, recordstable_1.editableInputType)(col.type);
|
|
564
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-recordpage-edit",
|
|
565
|
-
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
566
|
-
autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
|
|
567
|
-
if (isPending)
|
|
568
|
-
return;
|
|
569
|
-
if (e.key === "Enter")
|
|
570
|
-
commit(record.id, col.key, e.currentTarget.value);
|
|
571
|
-
else if (e.key === "Escape")
|
|
572
|
-
closeEditing();
|
|
573
|
-
}, onBlur: (e) => {
|
|
574
|
-
if (isPending)
|
|
575
|
-
return;
|
|
576
|
-
commit(record.id, col.key, e.currentTarget.value);
|
|
577
|
-
} }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
578
|
-
}
|
|
579
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t }) })) : ((0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
580
|
-
}), canRevealFields && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
|
|
581
|
-
// Revealing all fields also opens the (focused)
|
|
582
|
-
// field search; collapsing closes it.
|
|
583
|
-
const next = !showAllValues;
|
|
584
|
-
setShowAllValues(next);
|
|
585
|
-
setSearchOpen(next);
|
|
586
|
-
if (!next)
|
|
587
|
-
setFieldQuery("");
|
|
588
|
-
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
662
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsx)(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
589
663
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
590
664
|
return;
|
|
591
665
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -66,6 +66,11 @@ const config = (0, core_1.defineRecipe)({
|
|
|
66
66
|
& .crisp-recordpage-detailsearch { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: ${tokens_1.contract.size.chip}; flex-shrink: 0; padding: 0; margin: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
|
|
67
67
|
& .crisp-recordpage-detailsearch:hover, & .crisp-recordpage-detailsearch[data-active] { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
|
|
68
68
|
& .crisp-recordpage-noresults { padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
|
|
69
|
+
/* A caller-supplied \`sections\` grouping (#528) is not an Attio-measured
|
|
70
|
+
shape (its own Details column is flat) -- this heading reuses
|
|
71
|
+
.crisp-recordpage-section-title's own type treatment verbatim, just
|
|
72
|
+
spaced from the group above it. */
|
|
73
|
+
& .crisp-recordpage-fieldsection-title { margin-top: ${tokens_1.contract.space["3"]}; }
|
|
69
74
|
& .crisp-recordpage-morevalues { display: inline-flex; align-items: center; padding: 0; margin: 0; border: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
|
|
70
75
|
& .crisp-recordpage-morevalues:hover { color: ${tokens_1.contract.fgPrimary}; }
|
|
71
76
|
& .crisp-recordpage-viewall { flex-basis: 100%; margin-top: ${tokens_1.contract.space["0.5"]}; border: none; background: none; padding: 0; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.regular}; cursor: pointer; text-align: start; }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskList = TaskList;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const core_1 = require("../../core");
|
|
5
6
|
const tasklist_recipe_1 = require("./tasklist.recipe");
|
|
6
7
|
function Row({ task, onToggle, completeLabel, incompleteLabel, }) {
|
|
7
8
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tasklist-row", "data-done": task.done ? "" : undefined, children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", className: "crisp-tasklist-check", checked: !!task.done, "aria-label": task.done ? incompleteLabel : completeLabel, onChange: () => onToggle?.(task.id, !task.done) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tasklist-title", children: task.title }), (task.due != null || task.meta != null) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tasklist-meta", children: [task.due != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tasklist-due", "data-overdue": task.overdue ? "" : undefined, children: task.due })), task.meta] }))] }));
|
|
@@ -11,9 +12,15 @@ function Row({ task, onToggle, completeLabel, incompleteLabel, }) {
|
|
|
11
12
|
* count, then rows of a round complete-checkbox + title + trailing meta.
|
|
12
13
|
* Completed rows strike through; overdue dates go red.
|
|
13
14
|
*/
|
|
14
|
-
function TaskList({ tasks, groups, onToggle, emptyLabel
|
|
15
|
+
function TaskList({ tasks, groups, onToggle, emptyLabel: emptyLabelProp, className, completeLabel: completeLabelProp, incompleteLabel: incompleteLabelProp, ...rest }) {
|
|
15
16
|
const resolved = groups ?? (tasks?.length ? [{ label: null, tasks }] : []);
|
|
16
17
|
const total = resolved.reduce((s, g) => s + g.tasks.length, 0);
|
|
18
|
+
// crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
|
|
19
|
+
// hand rather than through `useCrispMessage`, which is string-only.
|
|
20
|
+
const crispMessages = (0, core_1.useCrispMessages)();
|
|
21
|
+
const emptyLabel = emptyLabelProp ?? crispMessages["No tasks"] ?? "No tasks";
|
|
22
|
+
const completeLabel = (0, core_1.useCrispMessage)(completeLabelProp, "Mark complete");
|
|
23
|
+
const incompleteLabel = (0, core_1.useCrispMessage)(incompleteLabelProp, "Mark incomplete");
|
|
17
24
|
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tasklist_recipe_1.tasklistRecipe)({ className }), ...rest, children: total === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tasklist-empty", children: emptyLabel })) : (resolved.map((g, gi) => ((0, jsx_runtime_1.jsxs)("div", { children: [g.label != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tasklist-group", children: [g.label, (0, jsx_runtime_1.jsx)("span", { className: "crisp-tasklist-group-count", children: g.tasks.length })] })), g.tasks.map((t) => ((0, jsx_runtime_1.jsx)(Row, { task: t, onToggle: onToggle, completeLabel: completeLabel, incompleteLabel: incompleteLabel }, t.id)))] }, gi)))) }));
|
|
18
25
|
}
|
|
19
26
|
TaskList.displayName = "TaskList";
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
4
5
|
import { bulkbarRecipe } from "./bulkbar.recipe.js";
|
|
5
6
|
/**
|
|
6
7
|
* A floating bulk-action bar. Rises from the bottom-centre of its nearest
|
|
7
8
|
* positioned ancestor when a selection is active; render it conditionally
|
|
8
9
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
|
|
9
10
|
*/
|
|
10
|
-
export function BulkBar({ count, onClear, countLabel
|
|
11
|
+
export function BulkBar({ count, onClear, countLabel: countLabelProp, countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel: clearLabelProp, ...rest }) {
|
|
12
|
+
// crisp/#471: same resolution order as every other component's own.
|
|
13
|
+
const countLabel = useCrispMessage(countLabelProp, "Selected");
|
|
14
|
+
const clearLabel = useCrispMessage(clearLabelProp, "Clear selection");
|
|
11
15
|
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
12
16
|
}
|
|
13
17
|
BulkBar.displayName = "BulkBar";
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useFileUpload, } from "@seed-design/react-file-upload";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { useFocusAfterRemoval } from "../../core/index.js";
|
|
6
|
+
import { useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
|
|
7
7
|
import { fileuploadRecipe } from "./fileupload.recipe.js";
|
|
8
8
|
export const fileUploadLabels = {
|
|
9
9
|
// Matches the copy on the record Files pane.
|
|
@@ -56,10 +56,26 @@ export function FileUpload({ className, labels, "aria-label": ariaLabel, id, acc
|
|
|
56
56
|
onAcceptedFileEntriesChange,
|
|
57
57
|
};
|
|
58
58
|
const [rejections, setRejections] = React.useState([]);
|
|
59
|
+
// crisp/#471: same resolution order as every other component's own —
|
|
60
|
+
// explicit `labels` entry wins, then a CrispMessagesProvider's
|
|
61
|
+
// translation of the built-in English default, then that default itself.
|
|
62
|
+
const title = useCrispMessage(labels?.title, fileUploadLabels.title);
|
|
63
|
+
const hint = useCrispMessage(labels?.hint, fileUploadLabels.hint);
|
|
64
|
+
const remove = useCrispMessage(labels?.remove, fileUploadLabels.remove);
|
|
65
|
+
const crispMessages = useCrispMessages();
|
|
59
66
|
const t = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
title,
|
|
68
|
+
hint,
|
|
69
|
+
remove,
|
|
70
|
+
errors: Object.fromEntries(Object.keys(fileUploadLabels.errors).map((code) => {
|
|
71
|
+
const englishDefault = fileUploadLabels.errors[code];
|
|
72
|
+
return [
|
|
73
|
+
code,
|
|
74
|
+
labels?.errors?.[code] ??
|
|
75
|
+
crispMessages[englishDefault] ??
|
|
76
|
+
englishDefault,
|
|
77
|
+
];
|
|
78
|
+
})),
|
|
63
79
|
};
|
|
64
80
|
const api = useFileUpload({
|
|
65
81
|
...props,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
3
4
|
import { formfieldRecipe } from "./formfield.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* Tell the control what the field knows about it.
|
|
@@ -60,12 +61,18 @@ function useFieldWiring({ htmlFor, messageId, invalid, required, }) {
|
|
|
60
61
|
export function FormField({ label, htmlFor, required, optional, description, error, children, className, ...rest }) {
|
|
61
62
|
const descId = React.useId();
|
|
62
63
|
const hasMsg = error != null || description != null;
|
|
64
|
+
// crisp/#471: `optional` doubles as a boolean flag and a ReactNode
|
|
65
|
+
// override — passing any node instead of `true` already renders it
|
|
66
|
+
// verbatim below, with no lookup needed. `true` reads a
|
|
67
|
+
// CrispMessagesProvider's translation of the built-in default text, same
|
|
68
|
+
// resolution order as every other component's own.
|
|
69
|
+
const optionalDefault = useCrispMessage(undefined, "(optional)");
|
|
63
70
|
useFieldWiring({
|
|
64
71
|
htmlFor,
|
|
65
72
|
messageId: hasMsg ? descId : undefined,
|
|
66
73
|
invalid: error != null,
|
|
67
74
|
required: !!required,
|
|
68
75
|
});
|
|
69
|
-
return (_jsxs("div", { className: formfieldRecipe({ className }), ...rest, children: [_jsxs("label", { className: "crisp-formfield-label", htmlFor: htmlFor, children: [label, required && (_jsx("span", { className: "crisp-formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && (_jsx("span", { className: "crisp-formfield-optional", children: optional === true ?
|
|
76
|
+
return (_jsxs("div", { className: formfieldRecipe({ className }), ...rest, children: [_jsxs("label", { className: "crisp-formfield-label", htmlFor: htmlFor, children: [label, required && (_jsx("span", { className: "crisp-formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && (_jsx("span", { className: "crisp-formfield-optional", children: optional === true ? optionalDefault : optional }))] }), children, error != null ? (_jsx("span", { className: "crisp-formfield-error", id: hasMsg ? descId : undefined, children: error })) : description != null ? (_jsx("span", { className: "crisp-formfield-desc", id: hasMsg ? descId : undefined, children: description })) : null] }));
|
|
70
77
|
}
|
|
71
78
|
FormField.displayName = "FormField";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCrispMessages } from "../../core/index.js";
|
|
2
3
|
import { heatmapchartRecipe } from "./heatmapchart.recipe.js";
|
|
3
4
|
/**
|
|
4
5
|
* An NxM cell-matrix chart — GRC's own risk heatmap (impact x likelihood)
|
|
@@ -7,9 +8,20 @@ import { heatmapchartRecipe } from "./heatmapchart.recipe.js";
|
|
|
7
8
|
* the recipe's own doc comment for why (and for its measurement — or lack
|
|
8
9
|
* of one — against Attio).
|
|
9
10
|
*/
|
|
10
|
-
export function HeatmapChart({ data, rowAxis, colAxis, format = (cell) => (cell.value ? cell.value : ""), cellTitle, onCellClick, isCellClickable = (cell) => cell.value > 0, empty, emptyLabel
|
|
11
|
+
export function HeatmapChart({ data, rowAxis, colAxis, format = (cell) => (cell.value ? cell.value : ""), cellTitle, onCellClick, isCellClickable = (cell) => cell.value > 0, empty, emptyLabel: emptyLabelProp, "aria-label": ariaLabel, className, ...rest }) {
|
|
11
12
|
const cols = data[0]?.length ?? 0;
|
|
12
|
-
|
|
13
|
+
// crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
|
|
14
|
+
// hand rather than through `useCrispMessage`, which is string-only. The
|
|
15
|
+
// default `aria-label` is a sentence built from two static fragments
|
|
16
|
+
// around live data (row/column counts, not translatable), so each
|
|
17
|
+
// fragment gets its own lookup — same as TableToolbar's "Sorted by"
|
|
18
|
+
// (crisp/#217).
|
|
19
|
+
const crispMessages = useCrispMessages();
|
|
20
|
+
const emptyLabel = emptyLabelProp ??
|
|
21
|
+
crispMessages["Nothing to plot yet."] ??
|
|
22
|
+
"Nothing to plot yet.";
|
|
23
|
+
const summary = ariaLabel ??
|
|
24
|
+
`${crispMessages.Heatmap ?? "Heatmap"}: ${data.length} ${crispMessages.x ?? "x"} ${cols} ${crispMessages.grid ?? "grid"}`;
|
|
13
25
|
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
26
|
const clickable = onCellClick != null && isCellClickable(cell, r, cc);
|
|
15
27
|
const title = cellTitle?.(cell, r, cc);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./skiplink.js";
|