@crisp-ui-kit/crisp 0.53.5 → 0.54.1
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/activityfeed.css +12 -0
- package/dist/activityfeed.d.ts +7 -0
- package/dist/activityfeed.layered.css +14 -0
- package/dist/badge.css +3 -0
- package/dist/badge.d.ts +2 -2
- package/dist/badge.layered.css +3 -0
- package/dist/cjs/components/badge/badge.recipe.js +10 -0
- 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 +5 -0
- package/dist/cjs/product/activityfeed/activityfeed.js +38 -0
- package/dist/cjs/product/activityfeed/activityfeed.recipe.js +35 -0
- package/dist/cjs/product/activityfeed/index.js +10 -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/richtext/richtext.js +43 -0
- package/dist/cjs/product/tasklist/tasklist.js +8 -1
- package/dist/cjs/tokens/palette.js +14 -0
- package/dist/cjs/tokens/semantic.js +5 -0
- package/dist/esm/components/badge/badge.recipe.js +10 -0
- 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 +5 -0
- package/dist/esm/product/activityfeed/activityfeed.js +32 -0
- package/dist/esm/product/activityfeed/activityfeed.recipe.js +32 -0
- package/dist/esm/product/activityfeed/index.js +2 -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/richtext/richtext.js +43 -0
- package/dist/esm/product/tasklist/tasklist.js +8 -1
- package/dist/esm/tokens/palette.js +14 -0
- package/dist/esm/tokens/semantic.js +5 -0
- package/dist/headerbar.css +7 -0
- package/dist/headerbar.d.ts +7 -0
- package/dist/headerbar.layered.css +9 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +16 -4
- 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 +69 -31
- package/dist/styles.layered.css +69 -31
- package/dist/textlink.css +11 -0
- package/dist/textlink.d.ts +7 -0
- package/dist/textlink.layered.css +13 -0
- package/dist/tokens.css +6 -0
- package/dist/tokens.json +16 -4
- package/dist/types/components/badge/badge.d.ts +2 -0
- package/dist/types/components/badge/badge.recipe.d.ts +8 -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 +5 -0
- package/dist/types/product/activityfeed/activityfeed.d.ts +57 -0
- package/dist/types/product/activityfeed/activityfeed.recipe.d.ts +3 -0
- package/dist/types/product/activityfeed/index.d.ts +2 -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/types/tokens/contract.d.ts +3 -0
- package/dist/types/tokens/palette.d.ts +12 -0
- package/dist/types/tokens/semantic.d.ts +5 -0
- package/dist/visuallyhidden.css +3 -0
- package/dist/visuallyhidden.d.ts +7 -0
- package/dist/visuallyhidden.layered.css +5 -0
- package/package.json +26 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { headerbarRecipe } from "./headerbar.recipe.js";
|
|
3
|
+
/**
|
|
4
|
+
* A page-level header row: an optional leading slot, a flexible title/
|
|
5
|
+
* breadcrumb region, and a trailing action cluster, on the measured 48px
|
|
6
|
+
* bar Attio uses for both its page-header and its view-bar row. See the
|
|
7
|
+
* recipe's own doc comment for the live measurement (crisp#462).
|
|
8
|
+
*/
|
|
9
|
+
export function HeaderBar({ leading, title, trailing, bordered = true, className, ...rest }) {
|
|
10
|
+
return (_jsxs("div", { className: headerbarRecipe({ className }), "data-bordered": bordered ? "" : undefined, ...rest, children: [leading ? (_jsx("span", { className: "crisp-headerbar-leading", children: leading })) : null, title ? _jsx("div", { className: "crisp-headerbar-title", children: title }) : null, trailing ? (_jsx("span", { className: "crisp-headerbar-trailing", children: trailing })) : null] }));
|
|
11
|
+
}
|
|
12
|
+
HeaderBar.displayName = "HeaderBar";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A page-level header row — measured live at app.attio.com/techpartners/companies
|
|
4
|
+
// against two stacked bars: the top utility bar (breadcrumb + trailing icon
|
|
5
|
+
// actions, e.g. search/notifications) and the view bar directly under it
|
|
6
|
+
// ("All Companies" view name + tabs + "New Company"). Both share the same
|
|
7
|
+
// 48px content height and `0 ${space3}` padding; only the outer (first) bar
|
|
8
|
+
// carries the divider -- measured `border-bottom: 1px solid rgb(238,239,241)`
|
|
9
|
+
// (`borderSubtle`/`line100`, exact match) on the utility bar, no border of
|
|
10
|
+
// its own on the view bar directly beneath it. `bordered` (default true)
|
|
11
|
+
// lets a consumer that stacks two HeaderBars turn it off on the inner one,
|
|
12
|
+
// the way GRC's own AppLayout UtilityBar + Page viewBar do today with two
|
|
13
|
+
// hand-rolled, drifting copies of the same 49px bar (crisp#462 GRC audit,
|
|
14
|
+
// crisp#527).
|
|
15
|
+
//
|
|
16
|
+
// No bell, no breadcrumb, no search box baked in -- those are the `leading`/
|
|
17
|
+
// `title`/`trailing` slot content a consumer supplies. `size.header` (48px)
|
|
18
|
+
// is reused rather than a new token, since it's the same "workspace-switcher
|
|
19
|
+
// header height" measured elsewhere in the sidebar.
|
|
20
|
+
const config = defineRecipe({
|
|
21
|
+
name: "headerbar",
|
|
22
|
+
base: `box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: ${c.space["2"]}; height: ${c.size.header}; flex-shrink: 0; padding: 0 ${c.space["3"]}; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
23
|
+
&[data-bordered] { border-bottom: 1px solid ${c.borderSubtle}; }
|
|
24
|
+
& .crisp-headerbar-leading { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; min-width: 0; }
|
|
25
|
+
& .crisp-headerbar-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: ${c.space["2"]}; overflow: hidden; }
|
|
26
|
+
& .crisp-headerbar-trailing { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; }`,
|
|
27
|
+
variants: {},
|
|
28
|
+
defaultVariants: {},
|
|
29
|
+
compoundVariants: [],
|
|
30
|
+
});
|
|
31
|
+
export const headerbarRecipe = resolveRecipe(config);
|
|
32
|
+
export default config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./headerbar.js";
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Bell } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useTablistKeyboard } from "../../components/tabs/index.js";
|
|
5
|
+
import { useCrispMessage, useCrispMessages } from "../../core/index.js";
|
|
5
6
|
import { inboxRecipe } from "./inbox.recipe.js";
|
|
6
7
|
/**
|
|
7
8
|
* A notifications inbox panel — measured from a notification flyout. A
|
|
@@ -9,8 +10,19 @@ import { inboxRecipe } from "./inbox.recipe.js";
|
|
|
9
10
|
* (avatar + message + time + unread dot) or a centred empty state. Put it inside
|
|
10
11
|
* a Popover for the flyout.
|
|
11
12
|
*/
|
|
12
|
-
export function Inbox({ notifications = [], title
|
|
13
|
+
export function Inbox({ notifications = [], title: titleProp, tabs, activeTab, onTabChange, emptyTitle: emptyTitleProp, emptySubtitle: emptySubtitleProp, emptyAction, tabsLabel: tabsLabelProp, className, ...rest }) {
|
|
13
14
|
const listRef = React.useRef(null);
|
|
15
|
+
// crisp/#471: `title`/`emptyTitle`/`emptySubtitle` are typed
|
|
16
|
+
// `React.ReactNode`, so they resolve by hand rather than through
|
|
17
|
+
// `useCrispMessage`, which is string-only — same reasoning as DataTable's
|
|
18
|
+
// own `empty`.
|
|
19
|
+
const crispMessages = useCrispMessages();
|
|
20
|
+
const title = titleProp ?? crispMessages.Notifications ?? "Notifications";
|
|
21
|
+
const emptyTitle = emptyTitleProp ?? crispMessages["No notifications"] ?? "No notifications";
|
|
22
|
+
const emptySubtitle = emptySubtitleProp ??
|
|
23
|
+
crispMessages["Notifications such as mentions and important updates will be displayed here."] ??
|
|
24
|
+
"Notifications such as mentions and important updates will be displayed here.";
|
|
25
|
+
const tabsLabel = useCrispMessage(tabsLabelProp, "Notification views");
|
|
14
26
|
// crisp's shared tablist model, the one `Tabs` runs (#195). Hand-rolling a
|
|
15
27
|
// second one is how Inbox ended up with no arrow keys, no roving tabindex
|
|
16
28
|
// and no name while `Tabs` had all three.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, MoreHorizontal } from "lucide-react";
|
|
3
|
+
import { useCrispMessage, useCrispMessages } from "../../core/index.js";
|
|
3
4
|
import { onboardingRecipe } from "./onboarding.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* The English defaults for the two pieces of copy that are formatters rather
|
|
@@ -15,9 +16,29 @@ export const onboardingLabels = {
|
|
|
15
16
|
* started" widget. Title + optional menu/minimize, a segmented progress bar,
|
|
16
17
|
* and step rows (completed rows strike through with a brand check).
|
|
17
18
|
*/
|
|
18
|
-
export function OnboardingChecklist({ title
|
|
19
|
+
export function OnboardingChecklist({ title: titleProp, steps, encouragement: encouragementProp, onStepClick, onMenu, onMinimize, className, optionsLabel: optionsLabelProp, minimizeLabel: minimizeLabelProp, progressLabel: progressLabelProp, progressText: progressTextProp, completedStepLabel: completedStepLabelProp, ...rest }) {
|
|
19
20
|
const total = steps.length;
|
|
20
21
|
const done = steps.filter((s) => s.done).length;
|
|
22
|
+
// crisp/#471: same resolution order as every other component's own —
|
|
23
|
+
// explicit prop, then a CrispMessagesProvider's translation of the English
|
|
24
|
+
// default, then that default itself. `title`/`encouragement` are typed
|
|
25
|
+
// `React.ReactNode` (a caller can pass a translated element), so they
|
|
26
|
+
// resolve by hand rather than through `useCrispMessage`, which is
|
|
27
|
+
// string-only — same reasoning as DataTable's own `empty`.
|
|
28
|
+
const crispMessages = useCrispMessages();
|
|
29
|
+
const title = titleProp ?? crispMessages["Getting started"] ?? "Getting started";
|
|
30
|
+
const encouragement = encouragementProp ?? crispMessages["Keep it up!"] ?? "Keep it up!";
|
|
31
|
+
const optionsLabel = useCrispMessage(optionsLabelProp, "Checklist options");
|
|
32
|
+
const minimizeLabel = useCrispMessage(minimizeLabelProp, "Minimize");
|
|
33
|
+
const progressLabel = useCrispMessage(progressLabelProp, "Onboarding progress");
|
|
34
|
+
// `progressText`/`completedStepLabel` are formatters, not strings (the
|
|
35
|
+
// count moves within the sentence between languages, #216) — each of
|
|
36
|
+
// their own static fragments gets its own lookup, same as
|
|
37
|
+
// TableToolbar's "Sorted by" (crisp/#217).
|
|
38
|
+
const progressText = progressTextProp ??
|
|
39
|
+
((doneCount, totalCount) => (_jsxs(_Fragment, { children: [_jsxs("b", { children: [doneCount, "/", totalCount] }), " ", crispMessages["steps completed"] ?? "steps completed"] })));
|
|
40
|
+
const completedStepLabel = completedStepLabelProp ??
|
|
41
|
+
((label) => `${String(label)} ${crispMessages["— completed"] ?? "— completed"}`);
|
|
21
42
|
return (_jsxs("div", { className: onboardingRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-onboarding-head", children: [_jsx("span", { className: "crisp-onboarding-title", children: title }), onMenu && (_jsx("button", { type: "button", className: "crisp-onboarding-iconbtn", "aria-label": optionsLabel, onClick: onMenu, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) })), onMinimize && (_jsx("button", { type: "button", className: "crisp-onboarding-iconbtn", "aria-label": minimizeLabel, onClick: onMinimize, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) }))] }), _jsxs("span", { className: "crisp-onboarding-sub", children: [progressText(done, total), encouragement ? _jsxs(_Fragment, { children: [" \u00B7 ", encouragement] }) : null] }), _jsx("div", { className: "crisp-onboarding-progress", role: "progressbar", "aria-valuenow": done, "aria-valuemin": 0, "aria-valuemax": total, "aria-label": progressLabel, children: Array.from({ length: total }, (_, i) => (_jsx("span", { className: "crisp-onboarding-seg", "data-done": i < done ? "" : undefined }, i))) }), _jsx("div", { className: "crisp-onboarding-steps", children: steps.map((s) => (_jsxs("button", { type: "button", className: "crisp-onboarding-step", "data-done": s.done ? "" : undefined, "aria-label": s.done ? completedStepLabel(s.label) : undefined, disabled: s.done, onClick: s.done ? undefined : () => onStepClick?.(s.id), children: [_jsx("span", { className: "crisp-onboarding-step-icon", children: s.icon }), _jsx("span", { className: "crisp-onboarding-step-label", children: s.label }), _jsx("span", { className: "crisp-onboarding-step-check", children: s.done && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, s.id))) })] }));
|
|
22
43
|
}
|
|
23
44
|
OnboardingChecklist.displayName = "OnboardingChecklist";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { RecordPage, recordPageLabels, } from "./recordpage.js";
|
|
1
|
+
export { RecordDetails, RecordPage, recordPageLabels, } from "./recordpage.js";
|
|
2
2
|
export { recordpageRecipe } from "./recordpage.recipe.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, useCrispComponentDefaults } from "../../core/index.js";
|
|
11
|
+
import { cx, 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) {
|
|
@@ -87,7 +87,42 @@ export const recordPageLabels = {
|
|
|
87
87
|
taggedCategories: (count) => (_jsxs(_Fragment, { children: ["Tagged ", _jsx("b", { children: count }), " categories"] })),
|
|
88
88
|
recordCreated: (name) => (_jsxs(_Fragment, { children: [_jsx("b", { children: name }), " record was created"] })),
|
|
89
89
|
noEmailsWith: (name) => _jsxs(_Fragment, { children: ["No emails logged with ", name, " yet."] }),
|
|
90
|
+
noFieldsMatch: (query) => _jsxs(_Fragment, { children: ["No fields match \u201C", query, "\u201D."] }),
|
|
90
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* crisp/#471: a `CrispMessagesProvider`'s `componentDefaults.recordPage` sits
|
|
94
|
+
* BETWEEN the built-in English defaults and an instance's own explicit
|
|
95
|
+
* `labels` prop -- a provider translates every `RecordPage`/`RecordDetails`
|
|
96
|
+
* on the page at once, and a caller who still passes `labels` for one
|
|
97
|
+
* instance keeps overriding it there. Not `useCrispMessage`: several of
|
|
98
|
+
* these entries (`domainSetTo`, `noFieldsMatch`, …) are formatter FUNCTIONS,
|
|
99
|
+
* which a flat string dictionary can't hold. Shared by `RecordPage` and
|
|
100
|
+
* `RecordDetails` (#528) -- both merge the same three layers the same way,
|
|
101
|
+
* so this is the one place that merge is written, not two copies of it.
|
|
102
|
+
*/
|
|
103
|
+
function useRecordPageLabels(labels) {
|
|
104
|
+
const providerLabels = useCrispComponentDefaults("recordPage");
|
|
105
|
+
return React.useMemo(() => ({
|
|
106
|
+
...recordPageLabels,
|
|
107
|
+
...providerLabels,
|
|
108
|
+
...labels,
|
|
109
|
+
tabs: {
|
|
110
|
+
...recordPageLabels.tabs,
|
|
111
|
+
...providerLabels?.tabs,
|
|
112
|
+
...labels?.tabs,
|
|
113
|
+
},
|
|
114
|
+
highlightLabels: {
|
|
115
|
+
...recordPageLabels.highlightLabels,
|
|
116
|
+
...providerLabels?.highlightLabels,
|
|
117
|
+
...labels?.highlightLabels,
|
|
118
|
+
},
|
|
119
|
+
highlightEmpty: {
|
|
120
|
+
...recordPageLabels.highlightEmpty,
|
|
121
|
+
...providerLabels?.highlightEmpty,
|
|
122
|
+
...labels?.highlightEmpty,
|
|
123
|
+
},
|
|
124
|
+
}), [labels, providerLabels]);
|
|
125
|
+
}
|
|
91
126
|
/** `Select` owns its own closed-by-default `open` state with no prop or
|
|
92
127
|
* imperative way to start it open -- the only thing that opens it is a real
|
|
93
128
|
* click on its trigger button. Mounting one on click (rather than rendering
|
|
@@ -143,6 +178,16 @@ function TagsValue({ values }) {
|
|
|
143
178
|
}
|
|
144
179
|
function FieldValue({ col, row, labels, }) {
|
|
145
180
|
const v = row[col.key];
|
|
181
|
+
// `render` (#528) -- the same full-custom-content escape hatch
|
|
182
|
+
// `RecordsTable`'s own columns already have (recordstable.tsx's own doc:
|
|
183
|
+
// "an escape hatch for a display crisp can't express via type"), reused
|
|
184
|
+
// here so a value this component has no built-in shape for (a reference
|
|
185
|
+
// field's link to another record, say) doesn't need a new `type`/`editor`
|
|
186
|
+
// of its own. Checked before the empty check -- like `RecordsTable`'s own
|
|
187
|
+
// `render`, it owns the cell's content fully, including what "empty"
|
|
188
|
+
// looks like for that column.
|
|
189
|
+
if (col.render)
|
|
190
|
+
return _jsx(_Fragment, { children: col.render(v, row) });
|
|
146
191
|
if (valueEmpty(v))
|
|
147
192
|
return _jsx("span", { className: "crisp-recordpage-empty", children: labels.setAValue });
|
|
148
193
|
// `editor: "select"` at rest (#457) -- checked ahead of `col.type` since
|
|
@@ -210,6 +255,147 @@ function TasksSection({ tasks, onAdd, onToggle, labels, }) {
|
|
|
210
255
|
function EmptyCard({ icon, children, }) {
|
|
211
256
|
return (_jsxs("div", { className: "crisp-recordpage-emptycard", children: [icon, children] }));
|
|
212
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The Details column's own field list, on its own (#528) — `RecordPage`'s
|
|
260
|
+
* "Record Details" collapsible + searchable field list, the one piece a
|
|
261
|
+
* consumer with a fixed-width rail of its own (no resize handle, no entity
|
|
262
|
+
* header, no CRM Lists panel — GRC's own persistent 320px column, grc#66)
|
|
263
|
+
* actually needs, rather than rebuilding this same click-to-edit/search/
|
|
264
|
+
* `sections` machinery by hand or pulling in the whole page's chrome for a
|
|
265
|
+
* rail that isn't one. `RecordPage` composes this internally for its own
|
|
266
|
+
* resizable aside (same component either way, not a second copy) — this is
|
|
267
|
+
* that same block, exactly, just not wrapped in `RecordPage`'s own topbar/
|
|
268
|
+
* tabs/resizer.
|
|
269
|
+
*
|
|
270
|
+
* Not wrapped in `recordpageRecipe` itself: doing so would duplicate the
|
|
271
|
+
* recipe's own `display:flex;height:100%` base rule as a nested child
|
|
272
|
+
* inside `RecordPage`'s aside (which already carries that class from
|
|
273
|
+
* `RecordPage`'s own root), breaking that layout. A standalone caller
|
|
274
|
+
* needs exactly one `.crisp-recordpage`-scoped ancestor for this
|
|
275
|
+
* component's classes to resolve — wrap it in one, the same way
|
|
276
|
+
* `RecordPage`'s own root does: `<div className={recordpageRecipe()}>`
|
|
277
|
+
* (also exported from this package).
|
|
278
|
+
*/
|
|
279
|
+
export function RecordDetails({ record, columns, sections, onEdit, labels, className, ...rest }) {
|
|
280
|
+
const t = useRecordPageLabels(labels);
|
|
281
|
+
const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = useEditableField(onEdit);
|
|
282
|
+
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
283
|
+
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
284
|
+
const [fieldQuery, setFieldQuery] = React.useState("");
|
|
285
|
+
const [showAllValues, setShowAllValues] = React.useState(false);
|
|
286
|
+
const fields = columns.filter((col) => col.type !== "entity");
|
|
287
|
+
const q = fieldQuery.trim().toLowerCase();
|
|
288
|
+
const matches = (col) => !q || col.label.toLowerCase().includes(q);
|
|
289
|
+
// #528: `sections` groups fields under headings, in the caller's own
|
|
290
|
+
// order, and always shows every match — no 5-field reveal (see
|
|
291
|
+
// `RecordPageSection`'s own doc). Omitted, this stays the flat,
|
|
292
|
+
// Attio-measured list with its 5-field "View all values" reveal,
|
|
293
|
+
// unchanged from before #528.
|
|
294
|
+
const FIELD_LIMIT = 5;
|
|
295
|
+
let canRevealFields = false;
|
|
296
|
+
let groups;
|
|
297
|
+
if (sections) {
|
|
298
|
+
const byKey = new Map(fields.map((col) => [col.key, col]));
|
|
299
|
+
groups = sections
|
|
300
|
+
.map((s) => ({
|
|
301
|
+
title: s.title,
|
|
302
|
+
fields: s.fields
|
|
303
|
+
.map((key) => byKey.get(key))
|
|
304
|
+
.filter((col) => col !== undefined)
|
|
305
|
+
.filter(matches),
|
|
306
|
+
}))
|
|
307
|
+
// A section left with no matching fields (a search that matches
|
|
308
|
+
// nothing in it, or a `fields` key naming no real column) doesn't
|
|
309
|
+
// render at all -- heading included, not an empty group under a
|
|
310
|
+
// heading with nothing below it.
|
|
311
|
+
.filter((g) => g.fields.length > 0);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
const matched = q ? fields.filter(matches) : fields;
|
|
315
|
+
canRevealFields = !q && matched.length > FIELD_LIMIT;
|
|
316
|
+
const shown = canRevealFields && !showAllValues
|
|
317
|
+
? matched.slice(0, FIELD_LIMIT)
|
|
318
|
+
: matched;
|
|
319
|
+
groups = [{ title: undefined, fields: shown }];
|
|
320
|
+
}
|
|
321
|
+
const totalShown = groups.reduce((n, g) => n + g.fields.length, 0);
|
|
322
|
+
return (_jsxs("div", { className: cx("crisp-recordpage-section", className), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
|
|
323
|
+
// biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
|
|
324
|
+
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) => {
|
|
325
|
+
if (e.key === "Escape") {
|
|
326
|
+
setSearchOpen(false);
|
|
327
|
+
setFieldQuery("");
|
|
328
|
+
}
|
|
329
|
+
} })) : (_jsxs("button", { type: "button", className: "crisp-recordpage-collapse", "data-collapsed": detailsOpen ? undefined : "", "aria-expanded": detailsOpen, onClick: () => setDetailsOpen((o) => !o), children: [t.recordDetails, _jsx(ChevronDown, { className: "crisp-recordpage-chev", size: 12, strokeWidth: 2.5 })] })), _jsx("button", { type: "button", className: "crisp-recordpage-detailsearch", "data-active": searchOpen ? "" : undefined, "aria-label": searchOpen ? t.closeSearch : t.searchFields, "aria-pressed": searchOpen, onClick: () => {
|
|
330
|
+
setSearchOpen((s) => !s);
|
|
331
|
+
setFieldQuery("");
|
|
332
|
+
if (!searchOpen)
|
|
333
|
+
setDetailsOpen(true);
|
|
334
|
+
}, children: searchOpen ? (_jsx(X, { size: 14, strokeWidth: 1.75 })) : (_jsx(Search, { size: 14, strokeWidth: 1.75 })) })] }), detailsOpen &&
|
|
335
|
+
(totalShown > 0 ? (_jsxs(_Fragment, { children: [groups.map((g, i) => (_jsxs(React.Fragment, { children: [sections && g.title != null && (_jsx("div", { className: "crisp-recordpage-section-title crisp-recordpage-fieldsection-title", children: g.title })), g.fields.map((col) => {
|
|
336
|
+
// #431: a field only actually opens when there's an
|
|
337
|
+
// `onEdit` to commit through -- `editable` with nothing
|
|
338
|
+
// to call would be a dead click.
|
|
339
|
+
const canEdit = Boolean(col.editable && onEdit);
|
|
340
|
+
const isPending = pendingKey === col.key;
|
|
341
|
+
const fieldError = error?.key === col.key ? error.message : null;
|
|
342
|
+
const label = (_jsxs("span", { className: "crisp-recordpage-field-label", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, _jsx("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
|
|
343
|
+
// `switch` is always its own live widget -- no
|
|
344
|
+
// click-to-open, no separate edit/view mode: flipping the
|
|
345
|
+
// toggle IS the commit gesture, and there's no
|
|
346
|
+
// meaningfully different "rest" display for a boolean
|
|
347
|
+
// (same exception `DataTable`'s own `editor` makes, #453).
|
|
348
|
+
if (canEdit && col.editor === "switch") {
|
|
349
|
+
const value = record[col.key];
|
|
350
|
+
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsxs("span", { className: "crisp-recordpage-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
351
|
+
}
|
|
352
|
+
// `select` opens crisp's own `Select` on a SINGLE click
|
|
353
|
+
// (#457, corrects #431/#385's original always-live copy
|
|
354
|
+
// of the same regression #453 fixed on `DataTable`) --
|
|
355
|
+
// like every other editable type here, at rest it shows
|
|
356
|
+
// its value (see `FieldValue`'s own `col.editor ===
|
|
357
|
+
// "select"` case) via the generic click-to-open branch
|
|
358
|
+
// below, and only mounts `Select` once `editingKey`
|
|
359
|
+
// matches. `DataTable`'s own double-click gate doesn't
|
|
360
|
+
// apply: `RecordPage`'s plain text/date fields already
|
|
361
|
+
// open on a single click (measured live for #431), so
|
|
362
|
+
// `select` matches that, not `DataTable`.
|
|
363
|
+
if (canEdit &&
|
|
364
|
+
editingKey === col.key &&
|
|
365
|
+
col.editor === "select") {
|
|
366
|
+
const value = record[col.key];
|
|
367
|
+
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsx(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
368
|
+
}
|
|
369
|
+
if (canEdit && editingKey === col.key) {
|
|
370
|
+
const value = record[col.key];
|
|
371
|
+
const editor = editableInputType(col.type);
|
|
372
|
+
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsx("input", { className: "crisp-recordpage-edit",
|
|
373
|
+
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
374
|
+
autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
|
|
375
|
+
if (isPending)
|
|
376
|
+
return;
|
|
377
|
+
if (e.key === "Enter")
|
|
378
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
379
|
+
else if (e.key === "Escape")
|
|
380
|
+
closeEditing();
|
|
381
|
+
}, onBlur: (e) => {
|
|
382
|
+
if (isPending)
|
|
383
|
+
return;
|
|
384
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
385
|
+
} }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" })), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
386
|
+
}
|
|
387
|
+
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? (_jsx("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: _jsx(FieldValue, { col: col, row: record, labels: t }) })) : (_jsx(FieldValue, { col: col, row: record, labels: t })), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
388
|
+
})] }, g.title != null ? String(g.title) : `section-${i}`))), canRevealFields && (_jsx("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
|
|
389
|
+
// Revealing all fields also opens the (focused) field
|
|
390
|
+
// search; collapsing closes it.
|
|
391
|
+
const next = !showAllValues;
|
|
392
|
+
setShowAllValues(next);
|
|
393
|
+
setSearchOpen(next);
|
|
394
|
+
if (!next)
|
|
395
|
+
setFieldQuery("");
|
|
396
|
+
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsx("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) })))] }));
|
|
397
|
+
}
|
|
398
|
+
RecordDetails.displayName = "RecordDetails";
|
|
213
399
|
/**
|
|
214
400
|
* The full-page company record view — the page that opens when you click into a
|
|
215
401
|
* record (as opposed to the peek [RecordPanel]). A top breadcrumb bar with
|
|
@@ -218,36 +404,8 @@ function EmptyCard({ icon, children, }) {
|
|
|
218
404
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
219
405
|
* into the app's main content area.
|
|
220
406
|
*/
|
|
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
|
-
const
|
|
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");
|
|
231
|
-
const t = React.useMemo(() => ({
|
|
232
|
-
...recordPageLabels,
|
|
233
|
-
...providerLabels,
|
|
234
|
-
...labels,
|
|
235
|
-
tabs: {
|
|
236
|
-
...recordPageLabels.tabs,
|
|
237
|
-
...providerLabels?.tabs,
|
|
238
|
-
...labels?.tabs,
|
|
239
|
-
},
|
|
240
|
-
highlightLabels: {
|
|
241
|
-
...recordPageLabels.highlightLabels,
|
|
242
|
-
...providerLabels?.highlightLabels,
|
|
243
|
-
...labels?.highlightLabels,
|
|
244
|
-
},
|
|
245
|
-
highlightEmpty: {
|
|
246
|
-
...recordPageLabels.highlightEmpty,
|
|
247
|
-
...providerLabels?.highlightEmpty,
|
|
248
|
-
...labels?.highlightEmpty,
|
|
249
|
-
},
|
|
250
|
-
}), [labels, providerLabels]);
|
|
407
|
+
export 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 }) {
|
|
408
|
+
const t = useRecordPageLabels(labels);
|
|
251
409
|
const [favorite, setFavorite] = React.useState(false);
|
|
252
410
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
253
411
|
// `[]`) -- otherwise RecordPage keeps its own self-contained state, as
|
|
@@ -259,11 +417,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
259
417
|
const notes = notesProp ?? internalNotes;
|
|
260
418
|
const tasks = tasksProp ?? internalTasks;
|
|
261
419
|
const [showAllActivity, setShowAllActivity] = React.useState(false);
|
|
262
|
-
// Collapsible + searchable Record Details.
|
|
263
|
-
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
264
|
-
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
265
|
-
const [fieldQuery, setFieldQuery] = React.useState("");
|
|
266
|
-
const [showAllValues, setShowAllValues] = React.useState(false);
|
|
267
420
|
// Drag-resizable left details column.
|
|
268
421
|
const [detailsW, setDetailsW] = React.useState(320);
|
|
269
422
|
const [resizing, setResizing] = React.useState(false);
|
|
@@ -300,17 +453,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
300
453
|
? record[entityCol.key]
|
|
301
454
|
: undefined;
|
|
302
455
|
const name = entity?.name ?? "Record";
|
|
303
|
-
const fields = columns.filter((col) => col.type !== "entity");
|
|
304
|
-
const q = fieldQuery.trim().toLowerCase();
|
|
305
|
-
const matchedFields = q
|
|
306
|
-
? fields.filter((col) => col.label.toLowerCase().includes(q))
|
|
307
|
-
: fields;
|
|
308
|
-
// Shows the first 5 fields; "View all values" reveals the rest.
|
|
309
|
-
const FIELD_LIMIT = 5;
|
|
310
|
-
const canRevealFields = !q && matchedFields.length > FIELD_LIMIT;
|
|
311
|
-
const shownFields = canRevealFields && !showAllValues
|
|
312
|
-
? matchedFields.slice(0, FIELD_LIMIT)
|
|
313
|
-
: matchedFields;
|
|
314
456
|
const byKey = React.useMemo(() => new Map(columns.map((col) => [col.key, col])), [columns]);
|
|
315
457
|
// The Overview "Highlights" — a fixed set of enrichment stats (measured
|
|
316
458
|
// from the live app), a couple derived from the record, the rest empty.
|
|
@@ -328,7 +470,11 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
328
470
|
: n >= 1000
|
|
329
471
|
? "1K–10K"
|
|
330
472
|
: "<1K";
|
|
331
|
-
|
|
473
|
+
// #528: the built-in CRM Highlights set, used when the caller omits
|
|
474
|
+
// `highlights` entirely -- a caller passing `highlights` (an array, or
|
|
475
|
+
// `false` to omit the section) replaces this wholesale, the same
|
|
476
|
+
// "caller's own list, not a merge" choice `tabs` already makes (#379).
|
|
477
|
+
const defaultHighlights = [
|
|
332
478
|
{
|
|
333
479
|
label: t.highlightLabels.connectionStrength,
|
|
334
480
|
icon: Zap,
|
|
@@ -366,6 +512,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
366
512
|
empty: t.highlightEmpty.employeeRange,
|
|
367
513
|
},
|
|
368
514
|
];
|
|
515
|
+
const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
|
|
369
516
|
const ti = { size: 15, strokeWidth: 1.75 };
|
|
370
517
|
const builtinTabs = [
|
|
371
518
|
{ value: "overview", label: t.tabs.overview, icon: _jsx(LayoutGrid, { ...ti }) },
|
|
@@ -448,7 +595,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
448
595
|
const builtinPanel = (active) => {
|
|
449
596
|
switch (active) {
|
|
450
597
|
case "overview":
|
|
451
|
-
return (_jsxs("div", { className: "crisp-recordpage-panel", children: [_jsxs("section", { children: [_jsx("div", { className: "crisp-recordpage-section-title", children: t.highlights }), _jsx("div", { className: "crisp-recordpage-hl-card", children: _jsx("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => (_jsxs("div", { className: "crisp-recordpage-hl-cell", children: [_jsx(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 }), _jsx("div", { className: "crisp-recordpage-hl-label", children: h.label }), _jsx("div", { className: "crisp-recordpage-hl-value", children: h.value ?? (_jsx("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] }), activityFeed, _jsxs("section", { children: [_jsx(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), _jsx(NotesSection, { notes: notes, onAdd: addNote, labels: t }), _jsx(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
|
|
598
|
+
return (_jsxs("div", { className: "crisp-recordpage-panel", children: [highlights !== false && (_jsxs("section", { children: [_jsx("div", { className: "crisp-recordpage-section-title", children: t.highlights }), _jsx("div", { className: "crisp-recordpage-hl-card", children: _jsx("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => (_jsxs("div", { className: "crisp-recordpage-hl-cell", children: [h.icon && (_jsx(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 })), _jsx("div", { className: "crisp-recordpage-hl-label", children: h.label }), _jsx("div", { className: "crisp-recordpage-hl-value", children: h.value ?? (_jsx("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] })), activityFeed, _jsxs("section", { children: [_jsx(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), _jsx(NotesSection, { notes: notes, onAdd: addNote, labels: t }), _jsx(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
|
|
452
599
|
case "activity":
|
|
453
600
|
return _jsx("div", { className: "crisp-recordpage-panel", children: activityFeed });
|
|
454
601
|
case "notes":
|
|
@@ -474,81 +621,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
474
621
|
const effectivePanel = tabsProp
|
|
475
622
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
476
623
|
: builtinPanel;
|
|
477
|
-
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsxs("
|
|
478
|
-
// biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
|
|
479
|
-
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) => {
|
|
480
|
-
if (e.key === "Escape") {
|
|
481
|
-
setSearchOpen(false);
|
|
482
|
-
setFieldQuery("");
|
|
483
|
-
}
|
|
484
|
-
} })) : (_jsxs("button", { type: "button", className: "crisp-recordpage-collapse", "data-collapsed": detailsOpen ? undefined : "", "aria-expanded": detailsOpen, onClick: () => setDetailsOpen((o) => !o), children: [t.recordDetails, _jsx(ChevronDown, { className: "crisp-recordpage-chev", size: 12, strokeWidth: 2.5 })] })), _jsx("button", { type: "button", className: "crisp-recordpage-detailsearch", "data-active": searchOpen ? "" : undefined, "aria-label": searchOpen ? t.closeSearch : t.searchFields, "aria-pressed": searchOpen, onClick: () => {
|
|
485
|
-
setSearchOpen((s) => !s);
|
|
486
|
-
setFieldQuery("");
|
|
487
|
-
if (!searchOpen)
|
|
488
|
-
setDetailsOpen(true);
|
|
489
|
-
}, children: searchOpen ? (_jsx(X, { size: 14, strokeWidth: 1.75 })) : (_jsx(Search, { size: 14, strokeWidth: 1.75 })) })] }), detailsOpen &&
|
|
490
|
-
(shownFields.length > 0 ? (_jsxs(_Fragment, { children: [shownFields.map((col) => {
|
|
491
|
-
// #431: a field only actually opens when there's an
|
|
492
|
-
// `onEdit` to commit through -- `editable` with nothing
|
|
493
|
-
// to call would be a dead click.
|
|
494
|
-
const canEdit = Boolean(col.editable && onEdit);
|
|
495
|
-
const isPending = pendingKey === col.key;
|
|
496
|
-
const fieldError = error?.key === col.key ? error.message : null;
|
|
497
|
-
const label = (_jsxs("span", { className: "crisp-recordpage-field-label", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, _jsx("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
|
|
498
|
-
// `switch` is always its own live widget -- no
|
|
499
|
-
// click-to-open, no separate edit/view mode: flipping the
|
|
500
|
-
// toggle IS the commit gesture, and there's no
|
|
501
|
-
// meaningfully different "rest" display for a boolean
|
|
502
|
-
// (same exception `DataTable`'s own `editor` makes, #453).
|
|
503
|
-
if (canEdit && col.editor === "switch") {
|
|
504
|
-
const value = record[col.key];
|
|
505
|
-
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsxs("span", { className: "crisp-recordpage-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
506
|
-
}
|
|
507
|
-
// `select` opens crisp's own `Select` on a SINGLE click
|
|
508
|
-
// (#457, corrects #431/#385's original always-live
|
|
509
|
-
// copy of the same regression #453 fixed on `DataTable`)
|
|
510
|
-
// -- like every other editable type here, at rest it
|
|
511
|
-
// shows its value (see `FieldValue`'s own `col.editor
|
|
512
|
-
// === "select"` case) via the generic click-to-open
|
|
513
|
-
// branch below, and only mounts `Select` once
|
|
514
|
-
// `editingKey` matches. `DataTable`'s own double-click
|
|
515
|
-
// gate doesn't apply: `RecordPage`'s plain text/date
|
|
516
|
-
// fields already open on a single click (measured live
|
|
517
|
-
// for #431), so `select` matches that, not `DataTable`.
|
|
518
|
-
if (canEdit &&
|
|
519
|
-
editingKey === col.key &&
|
|
520
|
-
col.editor === "select") {
|
|
521
|
-
const value = record[col.key];
|
|
522
|
-
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsx(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
523
|
-
}
|
|
524
|
-
if (canEdit && editingKey === col.key) {
|
|
525
|
-
const value = record[col.key];
|
|
526
|
-
const editor = editableInputType(col.type);
|
|
527
|
-
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsx("input", { className: "crisp-recordpage-edit",
|
|
528
|
-
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
529
|
-
autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
|
|
530
|
-
if (isPending)
|
|
531
|
-
return;
|
|
532
|
-
if (e.key === "Enter")
|
|
533
|
-
commit(record.id, col.key, e.currentTarget.value);
|
|
534
|
-
else if (e.key === "Escape")
|
|
535
|
-
closeEditing();
|
|
536
|
-
}, onBlur: (e) => {
|
|
537
|
-
if (isPending)
|
|
538
|
-
return;
|
|
539
|
-
commit(record.id, col.key, e.currentTarget.value);
|
|
540
|
-
} }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" })), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
541
|
-
}
|
|
542
|
-
return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? (_jsx("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: _jsx(FieldValue, { col: col, row: record, labels: t }) })) : (_jsx(FieldValue, { col: col, row: record, labels: t })), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
543
|
-
}), canRevealFields && (_jsx("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
|
|
544
|
-
// Revealing all fields also opens the (focused)
|
|
545
|
-
// field search; collapsing closes it.
|
|
546
|
-
const next = !showAllValues;
|
|
547
|
-
setShowAllValues(next);
|
|
548
|
-
setSearchOpen(next);
|
|
549
|
-
if (!next)
|
|
550
|
-
setFieldQuery("");
|
|
551
|
-
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsxs("div", { className: "crisp-recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _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) => {
|
|
624
|
+
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _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) => {
|
|
552
625
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
553
626
|
return;
|
|
554
627
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -63,6 +63,11 @@ const config = defineRecipe({
|
|
|
63
63
|
& .crisp-recordpage-detailsearch { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: ${c.size.chip}; flex-shrink: 0; padding: 0; margin: 0; border: none; border-radius: ${c.radius.sm}; background: none; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
|
|
64
64
|
& .crisp-recordpage-detailsearch:hover, & .crisp-recordpage-detailsearch[data-active] { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
65
65
|
& .crisp-recordpage-noresults { padding: ${c.space["1.5"]} ${c.space["1"]}; font-size: ${c.text.md}; color: ${c.fgMuted}; }
|
|
66
|
+
/* A caller-supplied \`sections\` grouping (#528) is not an Attio-measured
|
|
67
|
+
shape (its own Details column is flat) -- this heading reuses
|
|
68
|
+
.crisp-recordpage-section-title's own type treatment verbatim, just
|
|
69
|
+
spaced from the group above it. */
|
|
70
|
+
& .crisp-recordpage-fieldsection-title { margin-top: ${c.space["3"]}; }
|
|
66
71
|
& .crisp-recordpage-morevalues { display: inline-flex; align-items: center; padding: 0; margin: 0; border: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgMuted}; cursor: pointer; }
|
|
67
72
|
& .crisp-recordpage-morevalues:hover { color: ${c.fgPrimary}; }
|
|
68
73
|
& .crisp-recordpage-viewall { flex-basis: 100%; margin-top: ${c.space["0.5"]}; border: none; background: none; padding: 0; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); font-family: inherit; font-size: ${c.text.sm}; font-weight: ${c.weight.regular}; cursor: pointer; text-align: start; }
|